feat: 初始化

This commit is contained in:
jzx 2023-06-25 19:08:56 +08:00
parent 0bbb973250
commit 77ccb8cb52
482 changed files with 83285 additions and 0 deletions

23
config.json Executable file
View File

@ -0,0 +1,23 @@
{
"port": "3000",
"adminAccount": "admin@admin.com",
"timeout":120000,
"db": {
"servername": "127.0.0.1",
"DATABASE": "test",
"port": 27017,
"user": "test",
"pass": "test123",
"authSource": ""
},
"mail": {
"enable": true,
"host": "smtp.163.com",
"port": 465,
"from": "***@163.com",
"auth": {
"user": "***@163.com",
"pass": "*****"
}
}
}

0
init.lock Normal file
View File

15
log/2023-6.log Normal file
View File

@ -0,0 +1,15 @@
[ 2023/6/25 16:13:56 ] [ error ] MongoNetworkError: Authentication failed., mongodb Authentication failed
[ 2023/6/25 16:13:56 ] [ log ] mongodb load success...
[ 2023/6/25 16:17:28 ] [ error ] MongoNetworkError: Authentication failed., mongodb Authentication failed
[ 2023/6/25 16:17:28 ] [ log ] mongodb load success...
[ 2023/6/25 17:33:13 ] [ error ] MongoNetworkError: Authentication failed., mongodb Authentication failed
[ 2023/6/25 17:33:13 ] [ log ] mongodb load success...
[ 2023/6/25 17:33:51 ] [ error ] MongoNetworkError: Authentication failed., mongodb Authentication failed
[ 2023/6/25 17:33:51 ] [ log ] mongodb load success...
[ 2023/6/25 17:43:49 ] [ error ] MongoNetworkError: Authentication failed., mongodb Authentication failed
[ 2023/6/25 17:43:49 ] [ log ] mongodb load success...
[ 2023/6/25 17:46:24 ] [ log ] mongodb load success...
[ 2023/6/25 17:47:17 ] [ log ] -------------------------------------swaggerSyncUtils constructor-----------------------------------------------
[ 2023/6/25 17:47:17 ] [ log ] 服务已启动,请打开下面链接访问:
http://127.0.0.1:3000/
[ 2023/6/25 17:47:17 ] [ log ] mongodb load success...

2
vendors/.eslintignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/*
common/json-schema-mockjs.js

32
vendors/.eslintrc.js vendored Executable file
View File

@ -0,0 +1,32 @@
module.exports = {
env: {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
extends: ["eslint:recommended", "plugin:react/recommended"],
parser: "babel-eslint",
parserOptions: {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
plugins: [
"react",
"import"
],
rules: {
"indent": ["off", 2],
"react/display-name": ["off"],
"react/jsx-indent": ["error", 2],
"comma-dangle": ["error", "never"],
"no-console": ["off"],
"import/no-unresolved": ["off"],
"react/no-find-dom-node": ["off"],
"no-empty": ["off"]
// "react/no-unescaped-entities": 0
}
};

46
vendors/.gitignore vendored Executable file
View File

@ -0,0 +1,46 @@
# kdiff3 ignore
*.orig
# maven ignore
target/
# eclipse ignore
.settings/
.project
.classpath
.history
# idea ignore
.idea/
*.ipr
*.iml
*.iws
# temp ignore
*.log
*.cache
*.diff
*.patch
*.tmp
# system ignore
.DS_Store
Thumbs.db
# package ignore (optional)
# *.jar
# *.war
# *.zip
# *.tar
# *.tar.gz
node_modules/
runtime/
/prd/
/dev/
.tags
.tags1
tsconfig.json
client/plugin-module.js
.vscode
/iconfont

6
vendors/.npmignore vendored Normal file
View File

@ -0,0 +1,6 @@
/docs
/test
/static/doc
/iconfont
/ydoc.js
/ydocfile.js

1
vendors/.npmrc vendored Normal file
View File

@ -0,0 +1 @@
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/

139
vendors/.prettierrc.js vendored Normal file
View File

@ -0,0 +1,139 @@
module.exports = {
/**
* 设置prettier单行输出不折行最大长度
* 出于代码的可读性我们不推荐单行超过80个字符的coding方式
* 如果在格式markdown时不想折行请设置 prose wrap参数来禁止这一行为
* default: 80
*/
printWidth: 100,
/**
* 设置工具每一个水平缩进的空格数
* default: 2
*/
// tabWidth: 4,
/**
* 使用tab制表位缩进而非空格
* default: false
*/
useTabs: false,
/**
* 在语句末尾添加分号
* 有效参数
* true - 在每一条语句后面添加分号
* false - 只在有可能导致ASI错误的行首添加分号
* default: true
*/
semi: true,
/**
* 使用单引号而非双引号
* 在JSX语法中所有引号均为双引号该设置在JSX中被自动忽略
* default: false
*/
singleQuote: true,
/**
* default: "as-needed"
*/
quoteProps: "as-needed",
/**
* default: false
*/
jsxSingleQuote: true,
/**
* 在任何可能的多行中输入尾逗号
* none - 无尾逗号
* es5 - 添加es5中被支持的尾逗号
* all - 所有可能的地方都被添加尾逗号包括函数参数这个参数需要安装nodejs8或更高版本
* default: "none"
*/
trailingComma: "none",
/**
* 在对象字面量声明所使用的的花括号后{和前}输出空格
* true - Example: { foo: bar }
* false - Example: {foo: bar}
* default: true
*/
bracketSpacing: true,
/**
* 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行不适用于自闭和元素
* true - Example:
* <br
* onClick={this.handleClick} />
* false - Example:
* <br
* onClick={this.handleClick}
* />
* default: false
*/
jsxBracketSameLine: false,
/**
* 为单行箭头函数的参数添加圆括号
* avoid - 尽可能不添加圆括号示例x => x
* always - 总是添加圆括号示例 (x) => x
* default: "avoid"
*/
arrowParens: "avoid",
/**
* 只格式化某个文件的一部分
* 这两个参数可以用于从指定起止偏移字符(单独指定开始或结束两者同时指定分别指定)格式化代码
* 一下情况范围将会扩展
* 回退至包含选中语句的第一行的开始
* 向前直到选中语句的末尾
* 注意这些参数不可以同 cursorOffset 共用
* default: 0, Infinity
*/
rangeStart: 0,
rangeEnd: Infinity,
/**
* 指定使用哪一种解析器 docs: https://prettier.io/docs/en/options.html#parser
* default: None
*/
// parser: "None",
/**
* Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码这些注释
* 称为require pragma(必须杂注)这在逐步格式化一些大型未经格式化过的代码是十分有用的
* 示例 https://prettier.io/docs/en/options.html#require-pragma
* default: false
*/
requirePragma: false,
/**
* Prettier可以在文件的顶部插入一个 @format的特殊注释以表明改文件已经被Prettier格式化过了
* 在使用 --require-pragma 参数处理一连串的文件时这个功能将十分有用如果文件顶部
* 已经有一个doclock这个选项将新建一行注释并打上@format标记
* default: false
*/
insertPragma: false,
/**
* 默认情况下Prettier会因为使用了一些折行敏感型的渲染器如GitHub comment BitBucket而按照markdown文本样式进行折行但在某些情况下你可能只是希望这个文本在编译器或查看器中soft-wrapping是当屏幕放不下时发生的软折行所以这一参数允许设置为 " never "
* 有效参数
* always - 当超出print width上面有这个参数时就折行
* never - 不折行
* perserve - 按照文件原样折行 v1.9.0+
* default: "preserve"
*/
proseWrap: "preserve",
/**
* default: "css"
*/
htmlWhitespaceSensitivity: "css",
/**
* default: "auto"
*/
endOfLine: "auto"
};

647
vendors/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,647 @@
## [1.10.2](https://github.com/YMFE/yapi/compare/v1.10.1...v1.10.2) (2021-10-13)
### Bug Fixes
* [2361] 修复所有分类被删除时,同步问题. 解决方案: 增加 "默认分类" ([01207e2](https://github.com/YMFE/yapi/commit/01207e29cdceba2ea783d6e06b983cc34b283d51))
* issues 2357 ([5bab76c](https://github.com/YMFE/yapi/commit/5bab76c14429e7fb46f16d7b0d06851cf98b82e1))
### Features
* add nonexistent tags from `/interface/(add/up/save)` ([#1918](https://github.com/YMFE/yapi/issues/1918)) ([4d24c39](https://github.com/YMFE/yapi/commit/4d24c397483d2703e85c6977236270f52edf2d70))
## 1.10.1
### Bug Fixes
* 修复沙盒漏洞
## v1.9.2
* fix: 修复高级 mock 无效的bug
* opti: 对登录 email 空格的过滤
* fix: 修复deepMath 增加对原型属性比较
## v1.9.1
* 修复因 mongodb 一个废弃报错导致部署不成功问题
## v1.9.0
* 修复测试集合部分情况下闪动问题
* 修改ldap filter的匹配规则使其可以匹配&和|操作符 Merge pull request #1631 from vvkkhjt/master
* support switch_notice for /interface/save Merge pull request #1646 from tangcent/feature/openapi_notice
* 自动化测试时服务端测试node默认2分钟没有返回就直接断掉连接可以手动设置一个超时时间 Merge pull request #1675 from liugddx/master
* 修复导出的swagger.json 中 required 一直是 false 的 bug
* 修复 schema2json传required参数时导致faker失败
* 修改ldap filter的匹配规则
* 更新 sm2tsservice 3.2.0及以后版本的使用配置
## v1.8.8
* 更新了 cross-request [教程](https://juejin.im/post/5e3bbd986fb9a07ce152b53d),发布了最新的 3.1 版本
## v1.8.7
* 因 chrome 官方下架了 yapi 扩展,整理了本地安装教程
## v1.8.6
* 优化 swagger 文档导入分类策略,优先使用根路径的 tags 做分类,避免出现特别多分类的问题
## v1.8.5
* 改善 swagger 自动导入,不再支持秒级别的 cron 表达式,默认使用 10分钟更新一次的频率
* 修复输入空的 swagger地址 ,会发起请求的 bug
* 优化 swagger 数据导入,不会导入空的分类,不会使用版本号作为分类名称
* swagger 导入自动增加 basePath 到项目配置
## v1.8.4
* 修复 swagger 导入数据时,如果数据格式中缺少 in 的参数,会丢失请求参数
* 修复当传入数据格式的 method 不规范时,容易导致获取对象为空,出现异常导致动态页面无法打开
* 修复解决 json-schema-faker/json-schema-faker#453 问题
## v1.8.3
* 修复管理员无法看到所有分组的 bug
## v1.8.2
* 重构分组列表功能实现,大幅度优化首屏加载速度
* 接口运行界面设置 header、query、form 的初始值为其示例值
* 运行接口请求时支持自动预览HTML
### v1.8.1
* 优化插件【Swagger 自动同步】在添加地址时的服务端校验行为
* 优化单个测试用例执行超时时间限制,从3秒改为10秒
### v1.8.0
* filtering interface on the server instead of client
### v1.7.2
* 支持接口路径模糊搜索,不包含 basepath
### v1.7.1
* 废弃 yapi.ymfe.org 文档站点
### v1.7.0
* fix修复md两个undefined以及run_auto_test中执行用例id问题 #1024
### v1.7.0-beta.1
* 修复storage保存逻辑错误
### v1.7.0-beta.0
* **[插件]** 新增默认插件,支持通过 token 导出包含 basepath 的 json 格式接口,并整合添加 sm2tsservice 入口
* **[插件]** 新增默认插件支持swagger数据同步
* 修复不兼容 node7.6 bug
### v1.5.14
* 修复接口运行部分请求参数默认使用示例填写值导致无法删除参数bug
* 修复无法保存 global bug
### v1.5.13 存在bug
* 支持 pre-script 脚本持久化数据存储storage 兼容浏览器和服务端,并且是持久化数据存储,不会丢失,用法类似于 localStorage
* 修复了swagger 数据导入bug
* 修复接口运行部分请求参数默认使用示例填写值导致无法删除参数bug
### v1.5.12 存在bug
* 废弃 v1.6.x 新增功能因为有不可控的bug出现
* 支持项目设置 hook
* 开放api 新增 '/api/plugin/export'
* 接口运行部分请求参数默认使用示例填写值
### v1.5.10
* 解决 license should be a valid SPDX license expression 报错
* 修改OpenAPI比较版本方法
* fix复制路径不包含基本路径
* 修复了第一次部署,首页一直处于 loading bug
### v1.5.7
* 数据导入默认使用完全覆盖
* 升级新版本 cross-request 扩展,因 chrome 安全策略限制,不再支持文件上传
* fix 重复的 moment 依赖,导致安装时报错
* feat: add jsrsasign Lib
### v1.5.6
* 修复 /api/open/import_data 参数bug
* 修复 /api/open/import_data 文档错误merge 参数误写为 dataSync
### v1.5.5
* cross-request 升级到 2.10
* /api/open/import_data 新增 url 参数,支持服务端 url 导入
### v1.5.2
* 新增 openapi `/api/project/get`,可获取项目基本信息
### v1.5.1
* 优化 restful api 动态路由权重匹配算法,匹配更加精确
* openapi 新增 `/api/interface/list_cat`,获取某个分类下所有接口
* 新增了 rap数据导入到 yapi 插件 [rap2yapi](https://github.com/wxxcarl/yapi-plugin-import-rap)
### v1.5.0
* 优化开放 api功能现在 token 带有用户信息了
* 修复无法获取请求302 跳转前的 headers
### v1.4.4
* 优化了 json-schema 编辑器交互,修复了参数名写到一半提示重复的问题
* 优化了首页体验,提升页面打开速度
* 新增自动化测试通用规则配置功能
### v1.4.3
* 修复了可视化安装mongodb 报错的问题
* 支持了 swagger 导出功能
* 支持了克隆测试用例
### v1.4.2
* 优化数据导入对 headers 处理,如果 requestType 是 json自动增加header "content-type/json"
* fix: 修改了测试集合有多个项目接口时,切换执行环境相互覆盖不生效的问题 #692
* fix: mongoose warning 'Error: (node:3819) DeprecationWarning: collection.ensureIndex is deprecated. Use createIndexes instead'
* opti: 去掉没必要的redux-thunk
* 接口更新没有变化时不记录日志避免cron多次导入swagger的接口时导致动态里展示一大堆的无意义日志
### v1.4.1
* 支持任何人都可以添加分组,只有管理员才能修改项目是否公开
* 支持 mongodb 集群
#### Bug Fixed
* 修改 mock严格模式GET带有 JSON BODY 导致的验证问题
* 对 queryPath 改动导致的bug支持通过 xxx?controller=name 等 query 参数区分路径
* 因 tui-editor 需要安装github 依赖,导致部分机器无法部署成功的问题
### v1.3.23
* 接口tag功能
* 数据导入增加 merge 功能
* 增加参数的批量导入功能
* json schema 可视化编辑器增加 mock 功能
#### Bug Fixed
* 接口path中写入 ?name=xxx bug
* 高级mock 匹配 data: [{item: XXX}] 时匹配不成功
* 接口运行 query params 自动勾选
* mock get 带 cookie 时跨域
* json schema 嵌套多层 array 预览不展示 bug
* swagger URL 导入 跨域问题
### v1.3.22
* json schema number和integer支持枚举
* 服务端测试增加下载功能
* 增加 mock 接口请求字段参数验证
* 增加返回数据验证
#### Bug Fixed
* 命令行导入成员信息为 undefined
* 修复form 参数为空时 接口无法保存的问题
### v1.3.21
* 请求配置增加 context.utils.CryptoJS
* 环境变量支持自定义全局变量
* 增加wiki数据导出功能
* 用户管理处增加搜索功能
* 增加项目全局 mock 脚本功能
* 高级 mock 期望 支持关闭开启功能
#### Bug Fixed
* 优化ldap登陆
* swagger 导入公共params
* 接口编辑 mockEditor 修改为 AceEditor
### v1.3.20
#### Bug Fixed
* 修复 ykit 打包代码问题
* 修复 swagger url 导入选中后再切换其他数据方式时拖拽区域不出现问题
* 修复 wiki controller 后端报错问题
### v1.3.19
* 增加项目文档记录wiki
* 支持swagger URL 导入
* 接口运行和测试集合中加入参数备注信息
* 测试接口导入支持状态过滤
* json schema 增加枚举备注功能
* 左侧菜单栏可以支持单独滚动条
* 支持新版本通知
#### Bug Fixed
* 修复测试用例名称为空时保存测试用例出现的bug
* 导出markdown 路径参数处格式错误和参数table备注信息换行后样式错误
### v1.3.18
* 增加全局接口搜索功能
* 邮件通知过滤功能
#### Bug Fixed
* 新建接口自动添加为项目成员
* 修复type为raw header type 为form 时运行body 为空问题
* mongodb3.4-> 3.6 聚合 cursor报错
* path 路径支持
* json-schema 编辑器修复修改 type 导致描述信息被重置的问题
### v1.3.17
* 请求配置中添加 context.castId 字段用于标识测试用例
#### Bug Fixed
* 修复服务器端测试邮件通知开启token undefined bug
* 将状态由未完成修改成已完成之后原来的json格式的数据会变成json-schema
* 有分类为空时导出json后再导入报错
* 只修改参数 必需/非必需, 文本/文件 时, 查看改动详情, 提示没有改动
* ldap登陆允许用户输入的登陆账号非邮箱
### v1.3.16
* 支持自定义域名邮箱登录
* 测试用例支持导入不同项目接口
* 完善可视化表达式,可根据焦点编辑表达式
* req_body json 支持指针位置可视化插入表达式
#### Bug Fixed
* 导入postman headers 为 null 时报错
* format-data 数据解析不成功
* 导出的接口顺序按照api的接口顺序
### v1.3.15
* 增强跨域请求安全性,只允许 YApi 网站进行跨域请求
* 优化文档
* 修复 schema 描述信息展示 bug
* 增加禁止普通用户注册功能
### v1.3.14
* 修复接口编辑白屏问题
### v1.3.13
* 新增通过命令行导入 swagger 接口数据功能
* 接口请求设置新增异步处理特性
### v1.3.12
#### Feature
* 接口列表支持路径查询
* 项目复制
* 预览页面交互优化
* 优化服务端自动化测试文案
* 增加项目接口json数据导入导出功能
#### Bug Fixed
* 项目中访客权限的账号可以 增、删、改接口中高级mock的设置
* 高级Mock 中的响应时间值无法保存(实际提示为:保存成功)
* 分类为空时添加接口
### v1.3.11
#### Bug Fixed
* 修复 v1.3.10 websocket 连接问题
* 修复运行报错问题
* 修复数据导入 har 文件问题
### v1.3.9
#### Feature
* 增加接口编辑返回数据预览
* 修复旧的文档链接
#### Bug Fixed
* 导入数据为空提示
### v1.3.8
#### Feature
* 新增 json 结构可视化编辑器
* pre-script 增加 method 字段数据
#### Bug Fixed
* 点击编辑 tab 可能导致运行功能异常
* 修复postman导入没有分类的问题
* 修复postman参数导入缺失
### v1.3.7
#### Feature
* 完美支持 swagger, 接口请求参数和返回数据支持使用 json-schema 定义数据结构
* 增加测试集合列表的拖动功能
* 接口列表中增加“开放接口”状态
* 接口列表树形组件支持拖动
* json-schema 导出 table 表单
* 接口列表和测试集树形组件支持拖动
* 图标从阿里 cdn 替换到本地
#### Bug Fixed
* 修复高级 Mock 服务端报错
* 修复测试集合 table 拖动频繁请求的问题
* 修复 swagger 数据导入部分 bug
### v1.3.6
#### Feature
* 增加服务端的自动化测试功能,可集成到 jenkins, github 做接口自动化测试
* 增加导出公共接口功能
* 增加复制接口路径按钮
* 增加项目 token 功能,可通过 token 访问开放接口
* antd 升级到 v3
#### Bug Fixed
* 修复接口动态提示有误
* 修复变量表达式无法反向展示的问题
### v1.3.5
#### Feature
* 增加项目成员批量导入
* 数据导入同步,数据导入支持 swagger 3.0
* swagger 数据导入支持 2xx 的 httpcode
* 新增系统信息页面
#### Bug Fixed
* 修复离开接口编辑页面的 confirm 框有时候会触发两次 & confirm 的 X 按钮无效
* 修复添加集合后测试集合 list 不更新问题
* 测试集合点击对应接口侧边栏不切换
* 测试集合处,点击删除不成功
* 修改编辑接口后,再回到测试集合处数据不更新问题
* 修复 mongodb 帐号密码配置错误时引发的错误
* 修复删除分组后侧边数据没有更新问题
### v1.3.4
#### Feature
* 帮助文档首页增加部署公司
* 进入 project 页面加入 loading
* 接口 list 页 table 中加入分页
* 项目添加者自动变成项目组长
#### Bug Fixed
* 修复无权限进入项目 bug
* 修复复制接口query 等参数无法复制 bug
* 修复导出 html markdown 参数丢失问题
* 修复项目设置 pre-script 无法显示问题
### v1.3.3
#### Feature
* 邮件功能中: 1接口信息改动增加通知对应项目所有的成员2默认开启接口改动邮件提醒3) 增加邮件内容的 jsondiff 信息
#### Bug Fixed
* 优化接口运行页面插件提醒
* 完善 log 记录不到的问题
* 修复接口内容改动不发送邮件问题
* 修复部分 swagger 数据导入丢失问题
### v1.3.2
#### Feature
* 分组中新增接口自定义字段,便于用户在项目中添加额外字段数据
* 导入数据时新增导入 loading 显示
### v1.3.1
#### Bug Fixed
1. 修复接口状态和 req_params 参数无法更新问题
2. 修复搜索测试集合不展开问题
3. 修复测试过程中全局 header 不存在的问题
### v1.3.0
#### Feature
* yapi 默认集成 ldap 登录方式
* yapi 做一个 sso 登录插件,基于现有的 qsso 改造成大多数公司可用的
* 环境设置支持全局 header
* 接口运行页面选择环境增加管理环境的弹层
* 接口运行支持加工运行前后的 request 和 response ,主要是处理加密的接口或各种 token 参数问题
* 自动化测试除提供自定义脚本外,还提供可视化表单形式验证一些数据,例如 statusCode、bodyContent
* 增加查看接口详细改动
* 支持接口运行页面 body 全屏编辑
* 数据导出到 html 支持了分类
#### Bug Fixed
* 修复了高级 Mock 无法获取到真实客户端 ip
### v1.2.9
#### Bug Fixed
1. Api 路径兼容 postman {varible}
2. View Response Height 问题
#### Feature
1. 新增克隆测试集功能
2. 高级 Mock 期望支持 mockjs
3. pathname 允许只有一个 /
### v1.2.8
#### Bug Fixed
1. 修复接口运行 json 格式问题
2. 修复测试报告显示问题
3. 增加了接口数量统计
4. 多参数表达式改用双大括号{{}}
5. 修复了环境变量设置样式问题
#### Feature
1. 测试用例增加自定义测试脚本功能
### v1.2.7
#### Bug Fixed
1. 修复接口运行功能,当 httpCode 不是 200 时,导致无法获取 response body 问题
2. 修复路径参数无法删除优化测试集 table 页面,当文字超出一定长度会换行的问题
3. 优化测试集断言错误提示
4. 优化接口编辑 save 按钮样式
#### Feature
1. 测试集断言增加 log 方法,用于输出调试日志
2. 可视化动态参数表达式生成器,生成类似表达式 {@email | concat: pass | md5 | substr: 1,10}
### v1.2.6
#### Bug Fixed
1. 修复路径参数无法删除
#### Feature
1. 参数值支持多个动态参数,类似 str{@email}str{$.55.body.id}
2. 参数值支持管道表达式,例如 {@email | concat: pass | md5 | substr: 1,10}
3. 接口编辑参数**可拖动排序**
4. 修复路径参数无法删除问题
### v1.2.5
#### Bug Fixed
1. 成员如果第一次添加成员时选择组长,接着再添加下一个成员,如果 select 是默认的开发者,这时候会出现与上次 select 相同的值
2. 如果添加了一个不存在的成员还是会提示添加成功,并且发送的数据是原来发送成功的数据,这里需要重置初始值并在未找到对应用户名时对未找到的人名应该提示用户不存在
3. Fix 接口集自动化测试 header 没有解析 mock 和 变量参数
4. 在接口开发阶段,多个人并行改接口,如果最后一个人改之前没刷新页面,会把之前的人修改过的都冲掉了
5. 修复 cross-requestresponse header 字段重复 bug
#### Feature
1. 优化了分组添加,编辑交互
2. cross-request 计算了接口请求时间
3. 新增接口文档导出 html, markdown 功能
### v1.2.4
#### Bug Fixed
1. 期望值输入时候换成字符串,导致 diff 时因类型不一致匹配不上
2. swagger 导入数据时出现的 id 未定义 bug
3. fix: kerberos dependencies 导致安装依赖需要编译的问题
4. 修复了高级 mock 期望过滤参数为空时匹配不到的 bug
5. 将接口编辑页的保存按钮变成一直在窗口底部
6. 修改需求文档中项目操作处修改项目中的接口测试 a 链接指向的网页错误问题
7. 添加接口时重名,现在提示“已存在”,并在提示信息中告知用户删改接口的位置
8. 已添加的成员,再次添加会提示“添加成功”,优化提示为已成功添加人数,和已存在人数
9. 添加分组和修改分组时有个权限问题没有更新,切换列表才更新,该问题已解决
10. 解决修改和删除公共分类名称处,在添加接口时,选择接口分类名称没有修改的问题
#### Feature
1. 接口 path 支持了后面带 /
2. cross-request 支持了不安全的 header如 cookie, referer...
3. 支持了 path 带特殊符号"!"
4. 请求参数可改变顺序,目前只是对必需和非必需进行自动排序
5. 用户头像上传问题txt 改成 jpg 格式上传,用户头像显示空白,然后无法再次上传头像。无法再次上传的问题已经解决
6. 解决用户头像改变但是 header 处图片不变的问题。问题描述:用户上传头像成功但是 Header 处的头像没有改变,并且点击其他页面后再回到个人中心里面的头像又变成没有重新上传时的图片,必须重新刷新才可以将 Header 处的图片更新
7. 解决导入 postman 接口动态路由无法导入的 Bug
### v1.2.0
#### Features
* 增加高级 Mock 期望功能,根据设置的请求过滤规则,返回期望数据。
* 增加统计功能
* 增加自动化测试功能,写断言脚本,实现精准测试
#### Bug Fixed
* 修复了切换集合环境的 Bug
* 修复了 mockServer 拿不到 Post 请求 Body
* 修复了接口调试 pathParams 无法使用 mock 参数和变量参数
### v1.1.2
#### Features
* 接口运行增加了 query 和 body 的 enable 选项,可选择是否请求该字段
* Mock 支持了时间戳占位符 @timestamp
* 接口集运行页面可选择环境
* 接口集动态参数格式由原来的 $.{key}.{jsonPath} 改为 $.{key}.{body|params}.{jsonPath}
#### Bug Fixed
* 修复了接口集运行功能会忽略环境配置的 domain 路径
* 修复了动态路由 mock 返回结果不是该接口定义返回数据
* 修复了日志链接错误问题
* 修复了添加用户 loading 问题
* 修复了用户名编辑,前台未更新问题
* 修复了复制接口导致 GET 请求显示 request-body 问题
* 修复了接口集页面刷新后跳转到第一个接口集问题
* 修复了接口用例页面修改 header 参数值没有效果 bug
* 修复了接口集页面导入接口会导致 reqBody 清空 bug
### v1.1.1
#### Features
* 添加插件开发文档
* 接口和测试用例可拖动
* 优化动态提示
#### Bug Fixed
* 修复接口状态将接口方法重置为 get
* 环境配置域名带 path 无效
* 修复 Swagger 数据导入分类 bug
* MockServer 支持 CORS 跨域
* 优化 JSON-SCHEMA 转化为 JSON 的逻辑,由原来随机转换不被 required 字段改为转换全部字段
* 修复了项目成员无法看到该项目的 Bug
* 修复了无法查看公共项目的 Bug
* 优化了部分样式和交互
### v1.1.0
#### Features
* 新增个人空间功能,拥有这个分组的全部权限,可以在这个分组里探索 YApi 的功能
* 新增分组动态功能
* 优化接口运行页面交互
* CrossRequest 扩展支持 https
* 增加了 Swagger 数据导入功能
### v1.0.2
#### Features
* 网站改为 100%布局
* 优化搜索的提示
* 支持了 queryPath
* 接口浏览页面和编辑页面交互
* 新增高级 Mock 功能,可通过 js 代码去控制 mock 数据的生成
* 测试集支持了自动化测试
* 增加复制接口功能
* 在组长和开发者权限基础上,添加了 查看着 权限
### v1.0.1
#### Fix Bug
* 修改接口名字后,需要刷新页面左边的侧边栏才会显示正确的名字
* mockJson 出现 nullmock 出现格式不对问题
* 没有权限的分组不可选
* 项目列表图标设计大小优化下
* 关注的项目不显眼
* 添加接口之后,再次选择添加接口,会保留上次填写的信息
* 用例名称太长,导致无法使用删除功能
* 别人知道项目 id 虽然没有权限,但能看到里面所有内容
#### Features
* 接口备注集成了富文本编辑
* 支持 har 协议的接口数据导入
todo:
新增 crypto 加密函数

201
vendors/LICENSE vendored Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017, The YMFE Team.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

130
vendors/README.md vendored Executable file
View File

@ -0,0 +1,130 @@
## YApi 可视化接口管理平台
体验地址:
[http://yapi.smart-xwork.cn/](http://yapi.smart-xwork.cn/)
文档:
<p><a target="_blank" href="https://hellosean1025.github.io/yapi">hellosean1025.github.io/yapi</a></p>
### 平台介绍
![avatar](yapi-base-flow.jpg)
YApi 是<strong>高效</strong><strong>易用</strong><strong>功能强大</strong>的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 APIYApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
**QQ交流群**:
644642474 **主群可能已满**
941802405 **群2欢迎加入**
### 特性
* 基于 Json5 和 Mockjs 定义接口返回数据的结构和文档,效率提升多倍
* 扁平化权限设计,即保证了大型企业级项目的管理,又保证了易用性
* 类似 postman 的接口调试
* 自动化测试, 支持对 Response 断言
* MockServer 除支持普通的随机 mock 外,还增加了 Mock 期望功能,根据设置的请求过滤规则,返回期望数据
* 支持 postman, har, swagger 数据导入
* 免费开源,内网部署,信息再也不怕泄露了
### 内网部署
#### 环境要求
* nodejs7.6+)
* mongodb2.6+
* git
#### 安装
使用我们提供的 yapi-cli 工具,部署 YApi 平台是非常容易的。执行 yapi server 启动可视化部署程序,输入相应的配置和点击开始部署,就能完成整个网站的部署。部署完成之后,可按照提示信息,执行 node/{网站路径/server/app.js} 启动服务器。在浏览器打开指定url, 点击登录输入您刚才设置的管理员邮箱,默认密码为 ymfe.org 登录系统(默认密码可在个人中心修改)。
npm install -g yapi-cli --registry https://registry.npm.taobao.org
yapi server
#### 服务管理
利用pm2方便服务管理维护。
npm install pm2 -g //安装pm2
cd {项目目录}
pm2 start "vendors/server/app.js" --name yapi //pm2管理yapi服务
pm2 info yapi //查看服务信息
pm2 stop yapi //停止服务
pm2 restart yapi //重启服务
#### 升级
升级项目版本是非常容易的,并且不会影响已有的项目数据,只会同步 vendors 目录下的源码文件。
cd {项目目录}
yapi ls //查看版本号列表
yapi update //更新到最新版本
yapi update -v {Version} //更新到指定版本
### 教程
* [使用 YApi 管理 API 文档,测试, mock](https://juejin.im/post/5acc879f6fb9a028c42e8822)
* [自动更新 Swagger 接口数据到 YApi 平台](https://juejin.im/post/5af500e251882567096140dd)
* [自动化测试](https://juejin.im/post/5a388892f265da430e4f4681)
* [GTest(基于YApi)接口研发效能提升10倍 实战](https://mp.weixin.qq.com/s/z66f7bRX8aAOppAtBIB7Uw)
### YApi 插件
* [yapi sso 登录插件](https://github.com/YMFE/yapi-plugin-qsso)
* [yapi cas 登录插件](https://github.com/wsfe/yapi-plugin-cas) By wsfe
* [yapi gitlab集成插件](https://github.com/cyj0122/yapi-plugin-gitlab)
* [oauth2.0登录](https://github.com/xwxsee2014/yapi-plugin-oauth2)
* [rap平台数据导入](https://github.com/wxxcarl/yapi-plugin-import-rap)
* [dingding](https://github.com/zgs225/yapi-plugin-dding) 钉钉机器人推送插件
* [export-docx-data](https://github.com/inceptiongt/Yapi-plugin-export-docx-data) 数据导出docx文档
* [interface-oauth-token](https://github.com/shouldnotappearcalm/yapi-plugin-interface-oauth2-token) 定时自动获取鉴权token的插件
* [import-swagger-customize](https://github.com/follow-my-heart/yapi-plugin-import-swagger-customize) 导入指定swagger接口
### 代码生成
* [yapi-to-typescript根据 YApi 的接口定义生成 TypeScript 的请求函数](https://github.com/fjc0k/yapi-to-typescript)
* [yapi-gen-js-code: 根据 YApi 的接口定义生成 javascript 的请求函数](https://github.com/hellosean1025/yapi-gen-js-code)
* [SwiftJSONModeler:根据 YApi 的接口生成 Swift 模型代码](https://github.com/CodeOcenS/SwiftJSONModeler)
### YApi docker部署非官方
* [使用 alpine 版 docker 镜像快速部署 yapi](https://www.jianshu.com/p/a97d2efb23c5)
* [docker-yapi: 基于官方yapi-cli的docker-compose方案](https://github.com/Ryan-Miao/docker-yapi)
* [docker-compose一键部署yapi](https://github.com/jinfeijie/yapi)
* [docker-YApi: 更易用的 YApi 镜像](https://github.com/fjc0k/docker-YApi)
* [使用DockerCompose构建部署Yapi](https://github.com/MyHerux/daily-code/blob/master/Program/%E5%B7%A5%E5%85%B7%E7%AF%87/Yapi/%E4%BD%BF%E7%94%A8DockerCompose%E6%9E%84%E5%BB%BA%E9%83%A8%E7%BD%B2Yapi.md)
* [yapi-docker: dockerized yapi deployment all in one](https://github.com/williamlsh/yapi-docker)
### YApi 一些工具
* [Api Generator](https://github.com/Forgus/api-generator) 接口文档自动生成插件(零入侵)
* [mysql服务http工具,可配合做自动化测试](https://github.com/hellosean1025/http-mysql-server)
* [idea 一键上传接口到yapi插件](https://github.com/diwand/YapiIdeaUploadPlugin)
* [idea 接口上传调试插件 easy-yapi](https://easyyapi.com/)
* [执行 postgres sql 的服务](https://github.com/shouldnotappearcalm/http-postgres-server)
* [SpringBoot依赖自动生成YApi](https://github.com/NoBugBoy/YDoc)
* [Yapi X 一键生成接口文档, 上传到yapi, rap2, eolinker等IDEA插件](https://github.com/jetplugins/yapix)
### YApi 的一些客户
* 去哪儿
* 携程
* 艺龙
* 美团
* 百度
* 腾讯
* 阿里巴巴
* 京东
* 今日头条
* 唯品支付
* 链家网
* 快手
* 便利蜂
* 中商惠民
* 新浪
* VIPKID
* 马蜂窝
* 伴鱼
* 旷视科技
### Authors
* [hellosean1025](https://github.com/hellosean1025)
* [gaoxiaomumu](https://github.com/gaoxiaomumu)
* [zwjamnsss](https://github.com/amnsss)
* [dwb1994](https://github.com/dwb1994)
* [fungezi](https://github.com/fungezi)
* [ariesly15](https://github.com/ariesly15)
### License
Apache License 2.0

5
vendors/SECURITY.md vendored Normal file
View File

@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Please report security issues at js@liyi.im

156
vendors/client/Application.js vendored Executable file
View File

@ -0,0 +1,156 @@
import React, { PureComponent as Component } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Route, BrowserRouter as Router } from 'react-router-dom';
import { Home, Group, Project, Follows, AddProject, Login } from './containers/index';
import { Alert } from 'antd';
import User from './containers/User/User.js';
import Header from './components/Header/Header';
import Footer from './components/Footer/Footer';
import Loading from './components/Loading/Loading';
import MyPopConfirm from './components/MyPopConfirm/MyPopConfirm';
import { checkLoginState } from './reducer/modules/user';
import { requireAuthentication } from './components/AuthenticatedComponent';
import Notify from './components/Notify/Notify';
const plugin = require('client/plugin.js');
const LOADING_STATUS = 0;
const alertContent = () => {
const ua = window.navigator.userAgent,
isChrome = ua.indexOf('Chrome') && window.chrome;
if (!isChrome) {
return (
<Alert
style={{ zIndex: 99 }}
message={'YApi 的接口测试等功能仅支持 Chrome 浏览器,请使用 Chrome 浏览器获得完整功能。'}
banner
closable
/>
);
}
};
let AppRoute = {
home: {
path: '/',
component: Home
},
group: {
path: '/group',
component: Group
},
project: {
path: '/project/:id',
component: Project
},
user: {
path: '/user',
component: User
},
follow: {
path: '/follow',
component: Follows
},
addProject: {
path: '/add-project',
component: AddProject
},
login: {
path: '/login',
component: Login
}
};
// 增加路由钩子
plugin.emitHook('app_route', AppRoute);
@connect(
state => {
return {
loginState: state.user.loginState,
curUserRole: state.user.role
};
},
{
checkLoginState
}
)
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
login: LOADING_STATUS
};
}
static propTypes = {
checkLoginState: PropTypes.func,
loginState: PropTypes.number,
curUserRole: PropTypes.string
};
componentDidMount() {
this.props.checkLoginState();
}
showConfirm = (msg, callback) => {
// 自定义 window.confirm
// http://reacttraining.cn/web/api/BrowserRouter/getUserConfirmation-func
let container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(<MyPopConfirm msg={msg} callback={callback} />, container);
};
route = status => {
let r;
if (status === LOADING_STATUS) {
return <Loading visible />;
} else {
r = (
<Router getUserConfirmation={this.showConfirm}>
<div className="g-main">
<div className="router-main">
{this.props.curUserRole === 'admin' && <Notify />}
{alertContent()}
{this.props.loginState !== 1 ? <Header /> : null}
<div className="router-container">
{Object.keys(AppRoute).map(key => {
let item = AppRoute[key];
return key === 'login' ? (
<Route key={key} path={item.path} component={item.component} />
) : key === 'home' ? (
<Route key={key} exact path={item.path} component={item.component} />
) : (
<Route
key={key}
path={item.path}
component={requireAuthentication(item.component)}
/>
);
})}
</div>
{/* <div className="router-container">
<Route exact path="/" component={Home} />
<Route path="/group" component={requireAuthentication(Group)} />
<Route path="/project/:id" component={requireAuthentication(Project)} />
<Route path="/user" component={requireAuthentication(User)} />
<Route path="/follow" component={requireAuthentication(Follows)} />
<Route path="/add-project" component={requireAuthentication(AddProject)} />
<Route path="/login" component={Login} />
{/* <Route path="/statistic" component={statisticsPage} /> */}
{/* </div> */}
</div>
<Footer />
</div>
</Router>
);
}
return r;
};
render() {
return this.route(this.props.loginState);
}
}

238
vendors/client/common.js vendored Executable file
View File

@ -0,0 +1,238 @@
const moment = require('moment');
const constants = require('./constants/variable');
const Mock = require('mockjs');
const json5 = require('json5');
const MockExtra = require('common/mock-extra.js');
const Roles = {
0: 'admin',
10: 'owner',
20: 'dev',
30: 'guest',
40: 'member'
};
const roleAction = {
manageUserlist: 'admin',
changeMemberRole: 'owner',
editInterface: 'dev',
viewPrivateInterface: 'guest',
viewGroup: 'guest'
};
function isJson(json) {
if (!json) {
return false;
}
try {
json = JSON.parse(json);
return json;
} catch (e) {
return false;
}
}
exports.isJson = isJson;
function isJson5(json) {
if (!json) {
return false;
}
try {
json = json5.parse(json);
return json;
} catch (e) {
return false;
}
}
exports.safeArray = function(arr) {
return Array.isArray(arr) ? arr : [];
};
exports.json5_parse = function(json) {
try {
return json5.parse(json);
} catch (err) {
return json;
}
};
exports.json_parse = function(json) {
try {
return JSON.parse(json);
} catch (err) {
return json;
}
};
function deepCopyJson(json) {
return JSON.parse(JSON.stringify(json));
}
exports.deepCopyJson = deepCopyJson;
exports.isJson5 = isJson5;
exports.checkAuth = (action, role) => {
return Roles[roleAction[action]] <= Roles[role];
};
exports.formatTime = timestamp => {
return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss');
};
// 防抖函数,减少高频触发的函数执行的频率
// 请在 constructor 里使用:
// import { debounce } from '$/common';
// this.func = debounce(this.func, 400);
exports.debounce = (func, wait) => {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
};
};
// 从 Javascript 对象中选取随机属性
exports.pickRandomProperty = obj => {
let result;
let count = 0;
for (let prop in obj) {
if (Math.random() < 1 / ++count) {
result = prop;
}
}
return result;
};
exports.getImgPath = (path, type) => {
let rate = window.devicePixelRatio >= 2 ? 2 : 1;
return `${path}@${rate}x.${type}`;
};
function trim(str) {
if (!str) {
return str;
}
str = str + '';
return str.replace(/(^\s*)|(\s*$)/g, '');
}
exports.trim = trim;
exports.handlePath = path => {
path = trim(path);
if (!path) {
return path;
}
if (path === '/') {
return '';
}
path = path[0] !== '/' ? '/' + path : path;
path = path[path.length - 1] === '/' ? path.substr(0, path.length - 1) : path;
return path;
};
exports.handleApiPath = path => {
if (!path) {
return '';
}
path = trim(path);
path = path[0] !== '/' ? '/' + path : path;
return path;
};
// 名称限制 constants.NAME_LIMIT 字符
exports.nameLengthLimit = type => {
// 返回字符串长度汉字计数为2
const strLength = str => {
let length = 0;
for (let i = 0; i < str.length; i++) {
str.charCodeAt(i) > 255 ? (length += 2) : length++;
}
return length;
};
// 返回 form中的 rules 校验规则
return [
{
required: true,
validator(rule, value, callback) {
const len = value ? strLength(value) : 0;
if (len > constants.NAME_LIMIT) {
callback(
'请输入' + type + '名称,长度不超过' + constants.NAME_LIMIT + '字符(中文算作2字符)!'
);
} else if (len === 0) {
callback(
'请输入' + type + '名称,长度不超过' + constants.NAME_LIMIT + '字符(中文算作2字符)!'
);
} else {
return callback();
}
}
}
];
};
// 去除所有html标签只保留文字
exports.htmlFilter = html => {
let reg = /<\/?.+?\/?>/g;
return html.replace(reg, '') || '新项目';
};
// 实现 Object.entries() 方法
exports.entries = obj => {
let res = [];
for (let key in obj) {
res.push([key, obj[key]]);
}
return res;
};
exports.getMockText = mockTpl => {
try {
return JSON.stringify(Mock.mock(MockExtra(json5.parse(mockTpl), {})), null, ' ');
} catch (err) {
return '';
}
};
/**
* 合并后新的对象属性与 Obj 一致nextObj 有对应属性则取 nextObj 属性值否则取 Obj 属性值
* @param {Object} Obj 旧对象
* @param {Object} nextObj 新对象
* @return {Object} 合并后的对象
*/
exports.safeAssign = (Obj, nextObj) => {
let keys = Object.keys(nextObj);
return Object.keys(Obj).reduce((result, value) => {
if (keys.indexOf(value) >= 0) {
result[value] = nextObj[value];
} else {
result[value] = Obj[value];
}
return result;
}, {});
};
// 交换数组的位置
exports.arrayChangeIndex = (arr, start, end) => {
let newArr = [].concat(arr);
// newArr[start] = arr[end];
// newArr[end] = arr[start];
let startItem = newArr[start];
newArr.splice(start, 1);
// end自动加1
newArr.splice(end, 0, startItem);
let changes = [];
newArr.forEach((item, index) => {
changes.push({
id: item._id,
index: index
});
});
return changes;
};

View File

@ -0,0 +1,77 @@
import React from 'react';
import mockEditor from './mockEditor';
import PropTypes from 'prop-types';
import './AceEditor.scss';
const ModeMap = {
javascript: 'ace/mode/javascript',
json: 'ace/mode/json',
text: 'ace/mode/text',
xml: 'ace/mode/xml',
html: 'ace/mode/html'
};
const defaultStyle = { width: '100%', height: '200px' };
function getMode(mode) {
return ModeMap[mode] || ModeMap.text;
}
class AceEditor extends React.PureComponent {
constructor(props) {
super(props);
}
static propTypes = {
data: PropTypes.any,
onChange: PropTypes.func,
className: PropTypes.string,
mode: PropTypes.string, //enum[json, text, javascript], default is javascript
readOnly: PropTypes.bool,
callback: PropTypes.func,
style: PropTypes.object,
fullScreen: PropTypes.bool,
insertCode: PropTypes.func
};
componentDidMount() {
this.editor = mockEditor({
container: this.editorElement,
data: this.props.data,
onChange: this.props.onChange,
readOnly: this.props.readOnly,
fullScreen: this.props.fullScreen
});
let mode = this.props.mode || 'javascript';
this.editor.editor.getSession().setMode(getMode(mode));
if (typeof this.props.callback === 'function') {
this.props.callback(this.editor.editor);
}
}
componentWillReceiveProps(nextProps) {
if (!this.editor) {
return;
}
if (nextProps.data !== this.props.data && this.editor.getValue() !== nextProps.data) {
this.editor.setValue(nextProps.data);
let mode = nextProps.mode || 'javascript';
this.editor.editor.getSession().setMode(getMode(mode));
this.editor.editor.clearSelection();
}
}
render() {
return (
<div
className={this.props.className}
style={this.props.className ? undefined : this.props.style || defaultStyle}
ref={editor => {
this.editorElement = editor;
}}
/>
);
}
}
export default AceEditor;

View File

@ -0,0 +1,25 @@
.ace_editor.fullScreen {
height: auto;
width: auto;
border: 0;
margin: 0;
position: fixed !important;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1000008;
}
.ace_editor .ace_print-margin{
visibility: hidden !important
}
.fullScreen {
overflow: hidden
}
.ace_editor.ace-xcode {
background-color: #f5f5f5;
color: #000000;
}

View File

@ -0,0 +1,197 @@
var ace = require('brace'),
Mock = require('mockjs');
require('brace/mode/javascript');
require('brace/mode/json');
require('brace/mode/xml');
require('brace/mode/html');
require('brace/theme/xcode');
require('brace/ext/language_tools.js');
var json5 = require('json5');
const MockExtra = require('common/mock-extra.js');
var langTools = ace.acequire('ace/ext/language_tools'),
wordList = [
{ name: '字符串', mock: '@string' },
{ name: '自然数', mock: '@natural' },
{ name: '浮点数', mock: '@float' },
{ name: '字符', mock: '@character' },
{ name: '布尔', mock: '@boolean' },
{ name: 'url', mock: '@url' },
{ name: '域名', mock: '@domain' },
{ name: 'ip地址', mock: '@ip' },
{ name: 'id', mock: '@id' },
{ name: 'guid', mock: '@guid' },
{ name: '当前时间', mock: '@now' },
{ name: '时间戳', mock: '@timestamp' },
{ name: '日期', mock: '@date' },
{ name: '时间', mock: '@time' },
{ name: '日期时间', mock: '@datetime' },
{ name: '图片连接', mock: '@image' },
{ name: '图片data', mock: '@imageData' },
{ name: '颜色', mock: '@color' },
{ name: '颜色hex', mock: '@hex' },
{ name: '颜色rgba', mock: '@rgba' },
{ name: '颜色rgb', mock: '@rgb' },
{ name: '颜色hsl', mock: '@hsl' },
{ name: '整数', mock: '@integer' },
{ name: 'email', mock: '@email' },
{ name: '大段文本', mock: '@paragraph' },
{ name: '句子', mock: '@sentence' },
{ name: '单词', mock: '@word' },
{ name: '大段中文文本', mock: '@cparagraph' },
{ name: '中文标题', mock: '@ctitle' },
{ name: '标题', mock: '@title' },
{ name: '姓名', mock: '@name' },
{ name: '中文姓名', mock: '@cname' },
{ name: '中文姓', mock: '@cfirst' },
{ name: '中文名', mock: '@clast' },
{ name: '英文姓', mock: '@first' },
{ name: '英文名', mock: '@last' },
{ name: '中文句子', mock: '@csentence' },
{ name: '中文词组', mock: '@cword' },
{ name: '地址', mock: '@region' },
{ name: '省份', mock: '@province' },
{ name: '城市', mock: '@city' },
{ name: '地区', mock: '@county' },
{ name: '转换为大写', mock: '@upper' },
{ name: '转换为小写', mock: '@lower' },
{ name: '挑选(枚举)', mock: '@pick' },
{ name: '打乱数组', mock: '@shuffle' },
{ name: '协议', mock: '@protocol' }
];
let dom = ace.acequire('ace/lib/dom');
ace.acequire('ace/commands/default_commands').commands.push({
name: 'Toggle Fullscreen',
bindKey: 'F9',
exec: function(editor) {
if (editor._fullscreen_yapi) {
let fullScreen = dom.toggleCssClass(document.body, 'fullScreen');
dom.setCssClass(editor.container, 'fullScreen', fullScreen);
editor.setAutoScrollEditorIntoView(!fullScreen);
editor.resize();
}
}
});
function run(options) {
var editor, mockEditor, rhymeCompleter;
function handleJson(json) {
var curData = mockEditor.curData;
try {
curData.text = json;
var obj = json5.parse(json);
curData.format = true;
curData.jsonData = obj;
curData.mockData = () => Mock.mock(MockExtra(obj, {})); //为防止时时 mock 导致页面卡死的问题,改成函数式需要用到再计算
} catch (e) {
curData.format = e.message;
}
}
options = options || {};
var container, data;
container = options.container || 'mock-editor';
if (
options.wordList &&
typeof options.wordList === 'object' &&
options.wordList.name &&
options.wordList.mock
) {
wordList.push(options.wordList);
}
data = options.data || '';
options.readOnly = options.readOnly || false;
options.fullScreen = options.fullScreen || false;
editor = ace.edit(container);
editor.$blockScrolling = Infinity;
editor.getSession().setMode('ace/mode/javascript');
if (options.readOnly === true) {
editor.setReadOnly(true);
editor.renderer.$cursorLayer.element.style.display = 'none';
}
editor.setTheme('ace/theme/xcode');
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: false,
enableLiveAutocompletion: true,
useWorker: true
});
editor._fullscreen_yapi = options.fullScreen;
mockEditor = {
curData: {},
getValue: () => mockEditor.curData.text,
setValue: function(data) {
editor.setValue(handleData(data));
},
editor: editor,
options: options,
insertCode: code => {
let pos = editor.selection.getCursor();
editor.session.insert(pos, code);
}
};
function formatJson(json) {
try {
return JSON.stringify(JSON.parse(json), null, 2);
} catch (err) {
return json;
}
}
function handleData(data) {
data = data || '';
if (typeof data === 'string') {
return formatJson(data);
} else if (typeof data === 'object') {
return JSON.stringify(data, null, ' ');
} else {
return '' + data;
}
}
rhymeCompleter = {
identifierRegexps: [/[@]/],
getCompletions: function(editor, session, pos, prefix, callback) {
if (prefix.length === 0) {
callback(null, []);
return;
}
callback(
null,
wordList.map(function(ea) {
return { name: ea.mock, value: ea.mock, score: ea.mock, meta: ea.name };
})
);
}
};
langTools.addCompleter(rhymeCompleter);
mockEditor.setValue(handleData(data));
handleJson(editor.getValue());
editor.clearSelection();
editor.getSession().on('change', () => {
handleJson(editor.getValue());
if (typeof options.onChange === 'function') {
options.onChange.call(mockEditor, mockEditor.curData);
}
editor.clearSelection();
});
return mockEditor;
}
/**
* mockEditor({
container: 'req_body_json', //dom的id
data: that.state.req_body_json, //初始化数据
onChange: function (d) {
that.setState({
req_body_json: d.text
})
}
})
*/
module.exports = run;

View File

@ -0,0 +1,44 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { changeMenuItem } from '../reducer/modules/menu';
export function requireAuthentication(Component) {
return @connect(
state => {
return {
isAuthenticated: state.user.isLogin
};
},
{
changeMenuItem
}
)
class AuthenticatedComponent extends React.PureComponent {
constructor(props) {
super(props);
}
static propTypes = {
isAuthenticated: PropTypes.bool,
location: PropTypes.object,
dispatch: PropTypes.func,
history: PropTypes.object,
changeMenuItem: PropTypes.func
};
componentWillMount() {
this.checkAuth();
}
componentWillReceiveProps() {
this.checkAuth();
}
checkAuth() {
if (!this.props.isAuthenticated) {
this.props.history.push('/');
this.props.changeMenuItem('/');
}
}
render() {
return <div>{this.props.isAuthenticated ? <Component {...this.props} /> : null}</div>;
}
};
}

View File

@ -0,0 +1,42 @@
import './Breadcrumb.scss';
import { withRouter } from 'react-router-dom';
import { Breadcrumb } from 'antd';
import PropTypes from 'prop-types';
import React, { PureComponent as Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
@connect(state => {
return {
breadcrumb: state.user.breadcrumb
};
})
@withRouter
export default class BreadcrumbNavigation extends Component {
constructor(props) {
super(props);
}
static propTypes = {
breadcrumb: PropTypes.array
};
render() {
const getItem = this.props.breadcrumb.map((item, index) => {
if (item.href) {
return (
<Breadcrumb.Item key={index}>
<Link to={item.href}>{item.name}</Link>
</Breadcrumb.Item>
);
} else {
return <Breadcrumb.Item key={index}>{item.name}</Breadcrumb.Item>;
}
});
return (
<div className="breadcrumb-container">
<Breadcrumb>{getItem}</Breadcrumb>
</div>
);
}
}

View File

@ -0,0 +1,26 @@
@import '../../styles/mixin.scss';
.breadcrumb-container {
.ant-breadcrumb {
font-size: 16px;
float: left;
color: #fff;
padding-left: 16px;
line-height: unset;
}
.ant-breadcrumb a {
color: #fff;
&:hover {
color: $color-blue
}
}
.ant-breadcrumb > span:last-child {
color: #fff;
font-weight: normal;
}
.ant-breadcrumb-separator {
color: #fff;
font-weight: normal;
}
}

View File

@ -0,0 +1,100 @@
// 测试集合中的环境切换
import React from 'react';
import PropTypes from 'prop-types';
import { Select, Row, Col, Collapse, Icon, Tooltip } from 'antd';
const Option = Select.Option;
const Panel = Collapse.Panel;
import './index.scss';
export default class CaseEnv extends React.Component {
constructor(props) {
super(props);
}
static propTypes = {
envList: PropTypes.array,
currProjectEnvChange: PropTypes.func,
changeClose: PropTypes.func,
collapseKey: PropTypes.any,
envValue: PropTypes.object
};
callback = key => {
this.props.changeClose && this.props.changeClose(key);
};
render() {
return (
<Collapse
style={{
margin: 0,
marginBottom: '16px'
}}
onChange={this.callback}
// activeKey={this.state.activeKey}
activeKey={this.props.collapseKey}
>
<Panel
header={
<span>
{' '}
选择测试用例环境
<Tooltip title="默认使用测试用例选择的环境">
{' '}
<Icon type="question-circle-o" />{' '}
</Tooltip>
</span>
}
key="1"
>
<div className="case-env">
{this.props.envList.length > 0 && (
<div>
{this.props.envList.map(item => {
return (
<Row
key={item._id}
type="flex"
justify="space-around"
align="middle"
className="env-item"
>
<Col span={6} className="label">
<Tooltip title={item.name}>
<span className="label-name">{item.name}</span>
</Tooltip>
</Col>
<Col span={18}>
<Select
style={{
width: '100%'
}}
value={this.props.envValue[item._id] || ''}
defaultValue=""
onChange={val => this.props.currProjectEnvChange(val, item._id)}
>
<Option key="default" value="">
默认环境
</Option>
{item.env.map(key => {
return (
<Option value={key.name} key={key._id}>
{key.name + ': ' + key.domain}
</Option>
);
})}
</Select>
</Col>
</Row>
);
})}
</div>
)}
</div>
</Panel>
</Collapse>
);
}
}

View File

@ -0,0 +1,25 @@
.case-env {
.label {
// width: 100%;
text-align: right;
padding-right: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.label:after{
content: ":";
margin: 0 8px 0 2px;
position: relative;
top: -.5px;
}
.label-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.env-item {
margin-bottom: 16px;
}
}

View File

@ -0,0 +1,117 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
/**
* @author suxiaoxin
* @demo
* <EasyDragSort data={()=>this.state.list} onChange={this.handleChange} >
* {list}
* </EasyDragSot>
*/
let curDragIndex = null;
function isDom(obj) {
return (
obj &&
typeof obj === 'object' &&
obj.nodeType === 1 &&
typeof obj.nodeName === 'string' &&
typeof obj.getAttribute === 'function'
);
}
export default class EasyDragSort extends React.Component {
static propTypes = {
children: PropTypes.array,
onChange: PropTypes.func,
onDragEnd: PropTypes.func,
data: PropTypes.func,
onlyChild: PropTypes.string
};
render() {
const that = this;
const props = this.props;
const { onlyChild } = props;
let container = props.children;
const onChange = (from, to) => {
if (from === to) {
return;
}
let curValue;
curValue = props.data();
let newValue = arrMove(curValue, from, to);
if (typeof props.onChange === 'function') {
return props.onChange(newValue, from, to);
}
};
return (
<div>
{container.map((item, index) => {
if (React.isValidElement(item)) {
return React.cloneElement(item, {
draggable: onlyChild ? false : true,
ref: 'x' + index,
'data-ref': 'x' + index,
onDragStart: function() {
curDragIndex = index;
},
/**
* 控制 dom 是否可拖动
* @param {*} e
*/
onMouseDown(e) {
if (!onlyChild) {
return;
}
let el = e.target,
target = e.target;
if (!isDom(el)) {
return;
}
do {
if (el && isDom(el) && el.getAttribute(onlyChild)) {
target = el;
}
if (el && el.tagName == 'DIV' && el.getAttribute('data-ref')) {
break;
}
} while ((el = el.parentNode));
if (!el) {
return;
}
let ref = that.refs[el.getAttribute('data-ref')];
let dom = ReactDOM.findDOMNode(ref);
if (dom) {
dom.draggable = target.getAttribute(onlyChild) ? true : false;
}
},
onDragEnter: function() {
onChange(curDragIndex, index);
curDragIndex = index;
},
onDragEnd: function() {
curDragIndex = null;
if (typeof props.onDragEnd === 'function') {
props.onDragEnd();
}
}
});
}
return item;
})}
</div>
);
}
}
function arrMove(arr, fromIndex, toIndex) {
arr = [].concat(arr);
let item = arr.splice(fromIndex, 1)[0];
arr.splice(toIndex, 0, item);
return arr;
}

93
vendors/client/components/ErrMsg/ErrMsg.js vendored Executable file
View File

@ -0,0 +1,93 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { Icon } from 'antd';
import './ErrMsg.scss';
import { withRouter } from 'react-router';
/**
* 错误信息提示
*
* @component ErrMsg
* @examplelanguage js
*
* * 错误信息提示组件
* * 错误信息提示组件
*
*
*/
/**
* 标题
* 一般用于描述错误信息名称
* @property title
* @type string
* @description 一般用于描述错误信息名称
* @returns {object}
*/
@withRouter
class ErrMsg extends Component {
constructor(props) {
super(props);
}
static propTypes = {
type: PropTypes.string,
history: PropTypes.object,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
desc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
opration: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
};
render() {
let { type, title, desc, opration } = this.props;
let icon = 'frown-o';
if (type) {
switch (type) {
case 'noFollow':
title = '你还没有关注项目呢';
desc = (
<span>
先去 <a onClick={() => this.props.history.push('/group')}>项目广场</a> ,
那里可以添加关注
</span>
);
break;
case 'noInterface':
title = '该项目还没有接口呢';
desc = '在左侧 “接口列表” 中添加接口';
break;
case 'noMemberInProject':
title = '该项目还没有成员呢';
break;
case 'noMemberInGroup':
title = '该分组还没有成员呢';
break;
case 'noProject':
title = '该分组还没有项目呢';
desc = <span>请点击右上角添加项目按钮新建项目</span>;
break;
case 'noData':
title = '暂无数据';
desc = '先去别处逛逛吧';
break;
case 'noChange':
title = '没有改动';
desc = '该操作未改动 Api 数据';
icon = 'meh-o';
break;
default:
console.log('default');
}
}
return (
<div className="err-msg">
<Icon type={icon} className="icon" />
<p className="title">{title}</p>
<p className="desc">{desc}</p>
<p className="opration">{opration}</p>
</div>
);
}
}
export default ErrMsg;

20
vendors/client/components/ErrMsg/ErrMsg.scss vendored Executable file
View File

@ -0,0 +1,20 @@
.err-msg {
text-align: center;
font-size: .14rem;
line-height: 2;
margin-bottom: .24rem;
color: rgba(13, 27, 62, 0.43);
.icon {
font-size: .6rem;
margin-bottom: .08rem;
}
.title {
font-size: .18rem;
}
.desc {
}
.opration {
}
}

117
vendors/client/components/Footer/Footer.js vendored Executable file
View File

@ -0,0 +1,117 @@
import './Footer.scss';
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { Row, Col } from 'antd';
import { Icon } from 'antd';
const version = process.env.version;
class Footer extends Component {
constructor(props) {
super(props);
}
static propTypes = {
footList: PropTypes.array
};
render() {
return (
<div className="footer-wrapper">
<Row className="footer-container">
{this.props.footList.map(function(item, i) {
return (
<FootItem
key={i}
linkList={item.linkList}
title={item.title}
iconType={item.iconType}
/>
);
})}
</Row>
</div>
);
}
}
class FootItem extends Component {
constructor(props) {
super(props);
}
static propTypes = {
linkList: PropTypes.array,
title: PropTypes.string,
iconType: PropTypes.string
};
render() {
return (
<Col span={6}>
<h4 className="title">
{this.props.iconType ? <Icon type={this.props.iconType} className="icon" /> : ''}
{this.props.title}
</h4>
{this.props.linkList.map(function(item, i) {
return (
<p key={i}>
<a href={item.itemLink} className="link">
{item.itemTitle}
</a>
</p>
);
})}
</Col>
);
}
}
Footer.defaultProps = {
footList: [
{
title: 'GitHub',
iconType: 'github',
linkList: [
{
itemTitle: 'YApi 源码仓库',
itemLink: 'https://github.com/YMFE/yapi'
}
]
},
{
title: '团队',
iconType: 'team',
linkList: [
{
itemTitle: 'YMFE',
itemLink: 'https://ymfe.org'
}
]
},
{
title: '反馈',
iconType: 'aliwangwang-o',
linkList: [
{
itemTitle: 'Github Issues',
itemLink: 'https://github.com/YMFE/yapi/issues'
},
{
itemTitle: 'Github Pull Requests',
itemLink: 'https://github.com/YMFE/yapi/pulls'
}
]
},
{
title: `Copyright © 2018-${new Date().getFullYear()} YMFE`,
linkList: [
{
itemTitle: `版本: ${version} `,
itemLink: 'https://github.com/YMFE/yapi/blob/master/CHANGELOG.md'
},
{
itemTitle: '使用文档',
itemLink: 'https://hellosean1025.github.io/yapi/'
}
]
}
]
};
export default Footer;

65
vendors/client/components/Footer/Footer.scss vendored Executable file
View File

@ -0,0 +1,65 @@
@import '../../styles/common.scss';
@import '../../styles/mixin.scss';
.footer-wrapper{
height: 2.4rem;
width: 100%;
background-color: $color-bg-dark;
overflow: hidden;
position: relative;
z-index: 0;
}
.footer-container{
margin: 0 auto !important;
padding: .48rem .24rem;
max-width: 12.2rem;
.icon {
font-size: .16rem;
margin-right: .08rem;
}
.title {
color: #8898aa;
font-size: .14rem;
margin-bottom: .08rem;
}
.link {
font-size: .14rem;
font-weight: 200;
color: #8898aa;
line-height: .3rem;
transition: color .2s;
&:hover {
color: $color-bg-gray;
}
}
}
.footItem{
padding: 24px 2%;
width: 25%;
float: left;
div{
margin: 6px 0;
}
a{
font-weight: 200;
color: #b3bdc1;
&:hover{
color: white;
}
}
}
.copyRight{
padding: 24px 2%;
width: 25%;
float: left;
font-size: 13px;
text-indent: 1em;
h4{
font-size: 14px;
margin: 0 auto 13px;
font-weight: 500;
position: relative;
text-indent: 0;
}
}

View File

@ -0,0 +1,51 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { Button } from 'antd';
import { connect } from 'react-redux';
import { changeStudyTip, finishStudy } from '../../reducer/modules/user.js';
@connect(
null,
{
changeStudyTip,
finishStudy
}
)
class GuideBtns extends Component {
constructor(props) {
super(props);
}
static propTypes = {
changeStudyTip: PropTypes.func,
finishStudy: PropTypes.func,
isLast: PropTypes.bool
};
// 点击下一步
nextStep = () => {
this.props.changeStudyTip();
if (this.props.isLast) {
this.props.finishStudy();
}
};
// 点击退出指引
exitGuide = () => {
this.props.finishStudy();
};
render() {
return (
<div className="btn-container">
<Button className="btn" type="primary" onClick={this.nextStep}>
{this.props.isLast ? '完 成' : '下一步'}
</Button>
<Button className="btn" type="dashed" onClick={this.exitGuide}>
退出指引
</Button>
</div>
);
}
}
export default GuideBtns;

326
vendors/client/components/Header/Header.js vendored Executable file
View File

@ -0,0 +1,326 @@
import './Header.scss';
import React, { PureComponent as 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, Popover, Tag } from 'antd';
import { checkLoginState, logoutActions, loginTypeAction } from '../../reducer/modules/user';
import { changeMenuItem } from '../../reducer/modules/menu';
import { withRouter } from 'react-router';
import Srch from './Search/Search';
const { Header } = Layout;
import LogoSVG from '../LogoSVG/index.js';
import Breadcrumb from '../Breadcrumb/Breadcrumb.js';
import GuideBtns from '../GuideBtns/GuideBtns.js';
const plugin = require('client/plugin.js');
let HeaderMenu = {
user: {
path: '/user/profile',
name: '个人中心',
icon: 'user',
adminFlag: false
},
solution: {
path: '/user/list',
name: '用户管理',
icon: 'solution',
adminFlag: true
}
};
plugin.emitHook('header_menu', HeaderMenu);
const MenuUser = props => (
<Menu theme="dark" className="user-menu">
{Object.keys(HeaderMenu).map(key => {
let item = HeaderMenu[key];
const isAdmin = props.role === 'admin';
if (item.adminFlag && !isAdmin) {
return null;
}
return (
<Menu.Item key={key}>
{item.name === '个人中心' ? (
<Link to={item.path + `/${props.uid}`}>
<Icon type={item.icon} />
{item.name}
</Link>
) : (
<Link to={item.path}>
<Icon type={item.icon} />
{item.name}
</Link>
)}
</Menu.Item>
);
})}
<Menu.Item key="9">
<a onClick={props.logout}>
<Icon type="logout" />退出
</a>
</Menu.Item>
</Menu>
);
const tipFollow = (
<div className="title-container">
<h3 className="title">
<Icon type="star" /> 关注
</h3>
<p>这里是你的专属收藏夹便于你找到自己的项目</p>
</div>
);
const tipAdd = (
<div className="title-container">
<h3 className="title">
<Icon type="plus-circle" /> 新建项目
</h3>
<p>在任何页面都可以快速新建项目</p>
</div>
);
const tipDoc = (
<div className="title-container">
<h3 className="title">
使用文档 <Tag color="orange">推荐!</Tag>
</h3>
<p>
初次使用 YApi强烈建议你阅读{' '}
<a target="_blank" href="https://hellosean1025.github.io/yapi/" rel="noopener noreferrer">
使用文档
</a>
我们为你提供了通俗易懂的快速入门教程更有详细的使用说明欢迎阅读{' '}
</p>
</div>
);
MenuUser.propTypes = {
user: PropTypes.string,
msg: PropTypes.string,
role: PropTypes.string,
uid: PropTypes.number,
relieveLink: PropTypes.func,
logout: PropTypes.func
};
const ToolUser = props => {
let imageUrl = props.imageUrl ? props.imageUrl : `/api/user/avatar?uid=${props.uid}`;
return (
<ul>
<li className="toolbar-li item-search">
<Srch groupList={props.groupList} />
</li>
<Popover
overlayClassName="popover-index"
content={<GuideBtns />}
title={tipFollow}
placement="bottomRight"
arrowPointAtCenter
visible={props.studyTip === 1 && !props.study}
>
<Tooltip placement="bottom" title={'我的关注'}>
<li className="toolbar-li">
<Link to="/follow">
<Icon className="dropdown-link" style={{ fontSize: 16 }} type="star" />
</Link>
</li>
</Tooltip>
</Popover>
<Popover
overlayClassName="popover-index"
content={<GuideBtns />}
title={tipAdd}
placement="bottomRight"
arrowPointAtCenter
visible={props.studyTip === 2 && !props.study}
>
<Tooltip placement="bottom" title={'新建项目'}>
<li className="toolbar-li">
<Link to="/add-project">
<Icon className="dropdown-link" style={{ fontSize: 16 }} type="plus-circle" />
</Link>
</li>
</Tooltip>
</Popover>
<Popover
overlayClassName="popover-index"
content={<GuideBtns isLast={true} />}
title={tipDoc}
placement="bottomRight"
arrowPointAtCenter
visible={props.studyTip === 3 && !props.study}
>
<Tooltip placement="bottom" title={'使用文档'}>
<li className="toolbar-li">
<a target="_blank" href="https://hellosean1025.github.io/yapi" rel="noopener noreferrer">
<Icon className="dropdown-link" style={{ fontSize: 16 }} type="question-circle" />
</a>
</li>
</Tooltip>
</Popover>
<li className="toolbar-li">
<Dropdown
placement="bottomRight"
trigger={['click']}
overlay={
<MenuUser
user={props.user}
msg={props.msg}
uid={props.uid}
role={props.role}
relieveLink={props.relieveLink}
logout={props.logout}
/>
}
>
<a className="dropdown-link">
<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>
</li>
</ul>
);
};
ToolUser.propTypes = {
user: PropTypes.string,
msg: PropTypes.string,
role: PropTypes.string,
uid: PropTypes.number,
relieveLink: PropTypes.func,
logout: PropTypes.func,
groupList: PropTypes.array,
studyTip: PropTypes.number,
study: PropTypes.bool,
imageUrl: PropTypes.any
};
@connect(
state => {
return {
user: state.user.userName,
uid: state.user.uid,
msg: null,
role: state.user.role,
login: state.user.isLogin,
studyTip: state.user.studyTip,
study: state.user.study,
imageUrl: state.user.imageUrl
};
},
{
loginTypeAction,
logoutActions,
checkLoginState,
changeMenuItem
}
)
@withRouter
export default class HeaderCom extends Component {
constructor(props) {
super(props);
}
static propTypes = {
router: PropTypes.object,
user: PropTypes.string,
msg: PropTypes.string,
uid: PropTypes.number,
role: PropTypes.string,
login: PropTypes.bool,
relieveLink: PropTypes.func,
logoutActions: PropTypes.func,
checkLoginState: PropTypes.func,
loginTypeAction: PropTypes.func,
changeMenuItem: PropTypes.func,
history: PropTypes.object,
location: PropTypes.object,
study: PropTypes.bool,
studyTip: PropTypes.number,
imageUrl: PropTypes.any
};
linkTo = e => {
if (e.key != '/doc') {
this.props.changeMenuItem(e.key);
if (!this.props.login) {
message.info('请先登录', 1);
}
}
};
relieveLink = () => {
this.props.changeMenuItem('');
};
logout = e => {
e.preventDefault();
this.props
.logoutActions()
.then(res => {
if (res.payload.data.errcode == 0) {
this.props.history.push('/');
this.props.changeMenuItem('/');
message.success('退出成功! ');
} else {
message.error(res.payload.data.errmsg);
}
})
.catch(err => {
message.error(err);
});
};
handleLogin = e => {
e.preventDefault();
this.props.loginTypeAction('1');
};
handleReg = e => {
e.preventDefault();
this.props.loginTypeAction('2');
};
checkLoginState = () => {
this.props.checkLoginState
.then(res => {
if (res.payload.data.errcode !== 0) {
this.props.history.push('/');
}
})
.catch(err => {
console.log(err);
});
};
render() {
const { login, user, msg, uid, role, studyTip, study, imageUrl } = this.props;
return (
<Header className="header-box m-header">
<div className="content g-row">
<Link onClick={this.relieveLink} to="/group" className="logo">
<div className="href">
<span className="img">
<LogoSVG length="32px" />
</span>
</div>
</Link>
<Breadcrumb />
<div
className="user-toolbar"
style={{ position: 'relative', zIndex: this.props.studyTip > 0 ? 3 : 1 }}
>
{login ? (
<ToolUser
{...{ studyTip, study, user, msg, uid, role, imageUrl }}
relieveLink={this.relieveLink}
logout={this.logout}
/>
) : (
''
)}
</div>
</div>
</Header>
);
}
}

150
vendors/client/components/Header/Header.scss vendored Executable file
View File

@ -0,0 +1,150 @@
@import '../../styles/mixin.scss';
.nav-tooltip {
color: red;
}
.user-menu.ant-menu.ant-menu-dark .ant-menu-item-selected {
background: #32363a;
color: rgba(255, 255, 255, 0.67);
}
.user-menu.ant-menu-dark .ant-menu-item-selected > a {
color: rgba(255, 255, 255, 0.67);
}
/* .header-box.css */
.header-box {
height: .56rem;
line-height: .56rem;
padding: 0;
.logo {
position: relative;
float: left;
line-height: .56rem;
height: .56rem;
width: 56px;
border-right: 1px solid #55616d;
border-left: 1px solid #55616d;
background-color: inherit;
transition: all .2s;
&:hover{
background-color: #2395f1;
}
.href {
text-decoration: none;
display: block;
}
.logo-name {
color: $color-white;
font-size: .24rem;
font-weight: 300;
margin-left: .38rem;
}
.img {
position: absolute;
left: 0;
top: 50%;
left: 50%;
transform: translate(-16px,-17px);
}
.ui-badge {
position: absolute;
right: -18px;
top: 6px;
width: 30px;
height: 21px;
background-size: 236px 21px;
background-repeat: no-repeat;
background-image: none;
}
// &:before, &:after {
// content: '';
// display: block;
// width: 2px;
// height: .56rem;
// background-color: #222;
// border-left: 1px solid #575D67;
// position: relative;
// top: 0;
// }
// &:before {
// float: left;
// left: -.08rem;
// }
// &:after {
// float: right;
// right: -.27rem;
// }
}
.nav-toolbar {
font-size: .15rem;
float: left;
}
.user-menu{
margin-top: 20px;
}
.user-toolbar{
float: right;
height: .54rem;
display: flex;
align-items: center;
.item-search {
width: 2rem;
}
.toolbar-li{
float: left;
font-size: .14rem;
cursor: pointer;
color: #ccc;
margin-left: .16rem;
transition: color .2s;
& a {
color: #ccc;
}
.dropdown-link {
color: #ccc;
transition: color .2s;
// .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: 0;
border-radius: 16px;
}
.avatar-image > img{
height: auto;
width:100%;
display: bloack;
}
}
.anticon.active {
color: #2395f1;
}
&:hover{
.dropdown-link {
color: #2395f1;
}
}
.name {
margin-left: .08rem;
}
}
}
}

View File

@ -0,0 +1,158 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Icon, Input, AutoComplete } from 'antd';
import './Search.scss';
import { withRouter } from 'react-router';
import axios from 'axios';
import { setCurrGroup, fetchGroupMsg } from '../../../reducer/modules/group';
import { changeMenuItem } from '../../../reducer/modules/menu';
import { fetchInterfaceListMenu } from '../../../reducer/modules/interface';
const Option = AutoComplete.Option;
@connect(
state => ({
groupList: state.group.groupList,
projectList: state.project.projectList
}),
{
setCurrGroup,
changeMenuItem,
fetchGroupMsg,
fetchInterfaceListMenu
}
)
@withRouter
export default class Srch extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: []
};
}
static propTypes = {
groupList: PropTypes.array,
projectList: PropTypes.array,
router: PropTypes.object,
history: PropTypes.object,
location: PropTypes.object,
setCurrGroup: PropTypes.func,
changeMenuItem: PropTypes.func,
fetchInterfaceListMenu: PropTypes.func,
fetchGroupMsg: PropTypes.func
};
onSelect = async (value, option) => {
if (option.props.type === '分组') {
this.props.changeMenuItem('/group');
this.props.history.push('/group/' + option.props['id']);
this.props.setCurrGroup({ group_name: value, _id: option.props['id'] - 0 });
} else if (option.props.type === '项目') {
await this.props.fetchGroupMsg(option.props['groupId']);
this.props.history.push('/project/' + option.props['id']);
} else if (option.props.type === '接口') {
await this.props.fetchInterfaceListMenu(option.props['projectId']);
this.props.history.push(
'/project/' + option.props['projectId'] + '/interface/api/' + option.props['id']
);
}
};
handleSearch = value => {
axios
.get('/api/project/search?q=' + value)
.then(res => {
if (res.data && res.data.errcode === 0) {
const dataSource = [];
for (let title in res.data.data) {
res.data.data[title].map(item => {
switch (title) {
case 'group':
dataSource.push(
<Option
key={`分组${item._id}`}
type="分组"
value={`${item.groupName}`}
id={`${item._id}`}
>
{`分组: ${item.groupName}`}
</Option>
);
break;
case 'project':
dataSource.push(
<Option
key={`项目${item._id}`}
type="项目"
id={`${item._id}`}
groupId={`${item.groupId}`}
>
{`项目: ${item.name}`}
</Option>
);
break;
case 'interface':
dataSource.push(
<Option
key={`接口${item._id}`}
type="接口"
id={`${item._id}`}
projectId={`${item.projectId}`}
>
{`接口: ${item.title}`}
</Option>
);
break;
default:
break;
}
});
}
this.setState({
dataSource: dataSource
});
} else {
console.log('查询项目或分组失败');
}
})
.catch(err => {
console.log(err);
});
};
// getDataSource(groupList){
// const groupArr =[];
// groupList.forEach(item =>{
// groupArr.push("group: "+ item["group_name"]);
// })
// return groupArr;
// }
render() {
const { dataSource } = this.state;
return (
<div className="search-wrapper">
<AutoComplete
className="search-dropdown"
dataSource={dataSource}
style={{ width: '100%' }}
defaultActiveFirstOption={false}
onSelect={this.onSelect}
onSearch={this.handleSearch}
// filterOption={(inputValue, option) =>
// option.props.children.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
// }
>
<Input
prefix={<Icon type="search" className="srch-icon" />}
placeholder="搜索分组/项目/接口"
className="search-input"
/>
</AutoComplete>
</div>
);
}
}

View File

@ -0,0 +1,10 @@
$color-grey:#979DA7;
.search-wrapper{
cursor: auto;
.search-input{
width: 2rem;
}
.srch-icon{
}
}

95
vendors/client/components/Intro/Intro.js vendored Executable file
View File

@ -0,0 +1,95 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Icon } from 'antd';
import './Intro.scss';
import { OverPack } from 'rc-scroll-anim';
import TweenOne from 'rc-tween-one';
import QueueAnim from 'rc-queue-anim';
const IntroPart = props => (
<li className="switch-content">
<div className="icon-switch">
<Icon type={props.iconType} />
</div>
<div className="text-switch">
<p>
<b>{props.title}</b>
</p>
<p>{props.des}</p>
</div>
</li>
);
IntroPart.propTypes = {
title: PropTypes.string,
des: PropTypes.string,
iconType: PropTypes.string
};
class Intro extends React.PureComponent {
constructor(props) {
super(props);
}
static propTypes = {
intro: PropTypes.shape({
title: PropTypes.string,
des: PropTypes.string,
img: PropTypes.string,
detail: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string,
des: PropTypes.string
})
)
}),
className: PropTypes.string
};
render() {
const { intro } = this.props;
const id = 'motion';
const animType = {
queue: 'right',
one: { x: '-=30', opacity: 0, type: 'from' }
};
return (
<div className="intro-container">
<OverPack playScale="0.3">
<TweenOne
animation={animType.one}
key={`${id}-img`}
resetStyleBool
id={`${id}-imgWrapper`}
className="imgWrapper"
>
<div className="img-container" id={`${id}-img-container`}>
<img src={intro.img} />
</div>
</TweenOne>
<QueueAnim
type={animType.queue}
key={`${id}-text`}
leaveReverse
ease={['easeOutCubic', 'easeInCubic']}
id={`${id}-textWrapper`}
className={`${id}-text des-container textWrapper`}
>
<div key={`${id}-des-content`}>
<div className="des-title">{intro.title}</div>
<div className="des-detail">{intro.des}</div>
</div>
<ul className="des-switch" key={`${id}-des-switch`}>
{intro.detail.map(function(item, i) {
return (
<IntroPart key={i} title={item.title} des={item.des} iconType={item.iconType} />
);
})}
</ul>
</QueueAnim>
</OverPack>
</div>
);
}
}
export default Intro;

74
vendors/client/components/Intro/Intro.scss vendored Executable file
View File

@ -0,0 +1,74 @@
$imgUrl: "../../../static/image/";
$color-grey: #E5E5E5;
$color-blue: #2395f1;
$color-white: #fff;
.intro-container{
.imgWrapper{
height: 100%;
width: 50%;
overflow: hidden;
position: absolute;
left: 0;
}
.textWrapper{
display: block;
width: 50%;
height: 150px;
vertical-align: top;
position: absolute;
margin: auto;
right: 0;
}
.des-container{
padding-left: .15rem;
.des-title{
font-size: .24rem;
margin-bottom: .1rem;
}
.des-detail{
font-size: .15rem;
margin-bottom: .2rem;
}
.des-switch{
.switch-content{
float: left;
width: 50%;
max-height: .85rem;
font-size: .14rem;
padding: .1rem .15rem .1rem 0;
div{
float: left;
}
.icon-switch{
height: .4rem;
width: .4rem;
border-radius: .02rem;
background-color: $color-blue;
margin-right: .1rem;
color: $color-white;
display: flex;
align-items: center;
justify-content: center;
font-size: .18rem;
}
.text-switch{
width: calc(100% - .65rem);
}
}
}
}
.img-container{
height: 100%;
width: 100%;
padding-right: .15rem;
//background-image: url("#{$imgUrl}demo-img.png");
img{
height: 100%;
width: 100%;
border: .01rem solid $color-grey;
box-shadow : 0 0 3px 1px $color-grey;
border-radius: .04rem;
}
}
}

View File

@ -0,0 +1,65 @@
import React, { Component } from 'react';
import { Icon, Input, Tooltip } from 'antd';
import PropTypes from 'prop-types';
import './Label.scss';
export default class Label extends Component {
constructor(props) {
super(props);
this.state = {
inputShow: false,
inputValue: ''
};
}
static propTypes = {
onChange: PropTypes.func,
desc: PropTypes.string,
cat_name: PropTypes.string
};
toggle = () => {
this.setState({ inputShow: !this.state.inputShow });
};
handleChange = event => {
this.setState({ inputValue: event.target.value });
};
componentWillReceiveProps(nextProps) {
if (this.props.desc === nextProps.desc) {
this.setState({
inputShow: false
});
}
}
render() {
return (
<div>
{this.props.desc && (
<div className="component-label">
{!this.state.inputShow ? (
<div>
<p>
{this.props.desc} &nbsp;&nbsp;
<Tooltip title="编辑简介">
<Icon onClick={this.toggle} className="interface-delete-icon" type="edit" />
</Tooltip>
</p>
</div>
) : (
<div className="label-input-wrapper">
<Input onChange={this.handleChange} defaultValue={this.props.desc} size="small" />
<Icon
className="interface-delete-icon"
onClick={() => {
this.props.onChange(this.state.inputValue);
this.toggle();
}}
type="check"
/>
<Icon className="interface-delete-icon" onClick={this.toggle} type="close" />
</div>
)}
</div>
)}
</div>
);
}
}

View File

@ -0,0 +1,28 @@
.component-label {
p {
padding: 3px 7px;
&:hover {
.interface-delete-icon {
display: inline-block;
}
}
.interface-delete-icon {
display: none;
}
}
.interface-delete-icon {
&:hover {
color: #2395f1;
cursor: pointer;
}
}
.label-input-wrapper {
input {
width: 30%;
}
.interface-delete-icon {
font-size: 1.4em;
margin-left: 10px;
}
}
}

36
vendors/client/components/Loading/Loading.js vendored Executable file
View File

@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Loading.scss';
export default class Loading extends React.PureComponent {
static defaultProps = {
visible: false
};
static propTypes = {
visible: PropTypes.bool
};
constructor(props) {
super(props);
this.state = { show: props.visible };
}
componentWillReceiveProps(nextProps) {
this.setState({ show: nextProps.visible });
}
render() {
return (
<div className="loading-box" style={{ display: this.state.show ? 'flex' : 'none' }}>
<div className="loading-box-bg" />
<div className="loading-box-inner">
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
</div>
</div>
);
}
}

View File

@ -0,0 +1,88 @@
$ball-color:#30a1f2;
$ball-size:.15rem;
$ball-margin:.02rem;
$loader-radius:.25rem;
@function delay($interval, $count, $index) {
@return ($index * $interval) - ($interval * $count);
}
@keyframes ball-spin-fade-loader {
50% {
opacity: 0.3;
transform: scale(0.4);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@mixin ball-spin-fade-loader($n:8,$start:1){
@for $i from $start through $n {
> div:nth-child(#{$i}) {
$quarter: ($loader-radius/2) + ($loader-radius/5.5);
@if $i == 1 {
top: $loader-radius;
left: 0;
} @else if $i == 2 {
top: $quarter;
left: $quarter;
} @else if $i == 3 {
top: 0;
left: $loader-radius;
} @else if $i == 4 {
top: -$quarter;
left: $quarter;
} @else if $i == 5 {
top: -$loader-radius;
left: 0;
} @else if $i == 6 {
top: -$quarter;
left: -$quarter;
} @else if $i == 7 {
top: 0;
left: -$loader-radius;
} @else if $i == 8 {
top: $quarter;
left: -$quarter;
}
animation: ball-spin-fade-loader 1s delay(0.12s, $n, $i - 1) infinite linear;
}
}
}
.loading-box{
align-items: center;
justify-content: center;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 9999;
&-bg{
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
background: rgba(255,255,255,.7);
}
&-inner{
@include ball-spin-fade-loader();
position: relative;
>div{
position: absolute;
width: $ball-size;
height: $ball-size;
border-radius: 50%;
margin: $ball-margin;
background-color: $ball-color;
animation-fill-mode: both;
}
}
}

View File

@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from 'prop-types';
const LogoSVG = props => {
let length = props.length;
return (
<svg className="svg" width={length} height={length} 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 stopColor="#FFFFFF" offset="0%" />
<stop stopColor="#F2F2F2" offset="100%" />
</linearGradient>
<circle id="path-2" cx="31.9988602" cy="31.9988602" r="2.92886048" />
<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" />
<feGaussianBlur stdDeviation="1.5" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
<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"
/>
</filter>
</defs>
<g id="首页" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="大屏幕">
<g id="Icon">
<circle id="Oval-1" fill="url(#linearGradient-1)" cx="32" cy="32" r="32" />
<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"
fillRule="nonzero"
/>
<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"
fillRule="nonzero"
transform="translate(40.674608, 27.097010) rotate(60.000000) translate(-40.674608, -27.097010) "
/>
<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"
fillRule="nonzero"
transform="translate(23.392199, 27.040878) rotate(-60.000000) translate(-23.392199, -27.040878) "
/>
<g id="inner-round">
<use fill="black" fillOpacity="1" filter="url(#filter-3)" xlinkHref="#path-2" />
<use fill="#F7F7F7" fillRule="evenodd" xlinkHref="#path-2" />
</g>
</g>
</g>
</g>
</svg>
);
};
LogoSVG.propTypes = {
length: PropTypes.any
};
export default LogoSVG;

260
vendors/client/components/MockDoc/MockDoc.js vendored Executable file
View File

@ -0,0 +1,260 @@
import './MockDoc.scss';
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
// 组件用法 <MockDoc mock= mockData doc= docData />
// mockData: mock数据 格式为json
// docDatadocData数据 格式为array
class MockDoc extends Component {
constructor(props) {
super(props);
this.state = {
release: []
};
}
static propTypes = {
mock: PropTypes.object,
doc: PropTypes.array
};
// btnCol(start,col){
// return function(){
// console.log(start,col);
// }
// }
render() {
let htmlData = mockToArr(this.props.mock);
htmlData = arrToHtml(htmlData, this.props.doc);
return (
<div className="MockDoc">
{htmlData.map(function(item, i) {
{
/*//类型Object 必有字段 备注qwqwqw*/
}
if (item.mes) {
var mes = [];
item.mes.type
? mes.push(
<span key={i} className="keymes">
{' '}
/ /{item.mes.type}
</span>
)
: '';
item.mes.required
? mes.push(
<span key={i + 1} className="keymes">
必有字段
</span>
)
: '';
item.mes.desc
? mes.push(
<span key={i + 2} className="keymes">
备注{item.mes.desc}
</span>
)
: '';
}
return (
<div className="jsonItem" key={i}>
{<span className="jsonitemNum">{i + 1}.</span>}
{produceSpace(item.space)}
{setStrToHtml(item.str)}
{mes}
</div>
);
})}
</div>
);
}
}
MockDoc.defaultProps = {
mock: {
ersrcode: '@integer',
'data|9-19': [
'123',
{
name: '@name',
name1: [
{
name3: '1'
}
]
}
],
data1: '123',
data3: {
err: 'errCode',
arr: [1, 2]
}
},
doc: [
{ type: 'strisng', key: 'ersrcode', required: true, desc: '错误编码' },
{ type: 'number', key: 'data[]', required: true, desc: '返回数据' },
{ type: 'object', key: 'data[].name', required: true, desc: '数据名' },
{ type: 'object', key: 'data[].name1[].name3', required: true, desc: '数据名1' },
{ type: 'object', key: 'data1', required: true, desc: '数据名1' },
{ type: 'object', key: 'data3.err', required: true, desc: '数据名1' },
{ type: 'object', key: 'data3', required: true, desc: '数据名1' },
{ type: 'object', key: 'data3.arr[]', required: true, desc: '数据名1' }
]
};
function produceSpace(count) {
var space = [];
for (var i = 0; i < count; i++) {
space.push(<span key={i} className="spaces" />);
}
return space;
}
function setStrToHtml(str) {
return <span dangerouslySetInnerHTML={{ __html: `${str}` }} />;
}
function arrToHtml(mockArr, mock) {
for (var i in mockArr) {
for (var item in mock) {
// if(mockArr[i].key){
// console.log(mockArr[i].key,mock[item].key)
// }
if (mockArr[i].key && mockArr[i].key === mock[item].key) {
mockArr[i].mes = mock[item];
}
}
}
return mockArr;
}
function mockToArr(mock, html, space, key) {
html = html || [];
space = space || 0;
key = key || [];
if (typeof mock === 'object' && space === 0) {
if (mock.constructor === Array) {
html.push({
space: space,
str: '['
});
space++;
} else {
html.push({
space: space,
str: '{'
});
space++;
}
}
for (var i in mock) {
if (!mock.hasOwnProperty(i)) {
continue;
}
var index = i;
if (/^\w+(\|\w+)?/.test(i)) {
index = i.split('|')[0];
}
if (typeof mock[i] === 'object') {
if (mock[i].constructor === Array) {
// shuzu
if (mock.constructor != Array) {
if (key.length) {
key.push('.' + index + '[]');
} else {
key.push(index + '[]');
}
} else {
key.push('[]');
}
html.push({
space: space,
str: index + ' : [',
key: key.join('')
});
} else {
// object
if (mock.constructor != Array) {
if (key.length) {
key.push('.' + index);
} else {
key.push(index);
}
html.push({
space: space,
str: index + ' : {'
});
} else {
html.push({
space: space,
str: '{'
});
}
}
space++;
mockToArr(mock[i], html, space, key);
key.pop();
space--;
} else {
if (mock.constructor === Array) {
// html.push(produceSpace(space) + mock[i]+ ",");
html.push({
space: space,
str: `<span class = "valueLight">${mock[i]}</span>` + ','
});
} else {
// html.push(produceSpace(space) + index + ":" + mock[i] + ",");
if (mock.constructor != Array) {
if (key.length) {
// doc.push(key+"."+index);
html.push({
space: space,
str: index + ' : ' + `<span class = "valueLight">${mock[i]}</span>` + ',',
key: key.join('') + '.' + index
});
} else {
// doc.push(key + index);
html.push({
space: space,
str: index + ' : ' + `<span class = "valueLight">${mock[i]}</span>` + ',',
key: key.join('') + index
});
}
} else {
html.push({
space: space,
str: index + ' : ' + `<span class = "valueLight">${mock[i]}</span>` + ',',
key: key.join('')
});
}
}
}
}
if (typeof mock === 'object') {
html[html.length - 1].str = html[html.length - 1].str.substr(
0,
html[html.length - 1].str.length - 1
);
if (mock.constructor === Array) {
space--;
// html.push(produceSpace(space)+"]");
html.push({
space: space,
str: ']'
});
} else {
space--;
// html.push(produceSpace(space)+"}");
html.push({
space: space,
str: '}'
});
}
}
if (space != 0) {
html[html.length - 1].str = html[html.length - 1].str + ',';
}
return html;
}
export default MockDoc;

View File

@ -0,0 +1,31 @@
.MockDoc{
background-color: #F6F6F6;
// padding: 16px;
border:16px solid #F6F6F6;
overflow: auto;
width: 500px;
height: 500px;
word-break:keep-all; /* 不换行 */
white-space:nowrap;
.jsonItem{
font-size: 14px;
width: auto;
}
.jsonitemNum{
margin-right: 16px;
display: inline-block;
width: 25px;
border-right: 1px solid gray;
}
.valueLight{
color: #108ee9;
}
}
.spaces{
display: inline-block;
width: 30px;
}
.keymes{
margin-left: 20px;
}

View File

@ -0,0 +1,192 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Row, Icon, Input, Select, Tooltip } from 'antd';
import _ from 'underscore';
const Option = Select.Option;
// 深拷贝
function deepEqual(state) {
return JSON.parse(JSON.stringify(state));
}
const METHODS_LIST = [
{ name: 'md5', type: false, params: [], desc: 'md5加密' },
{ name: 'lower', type: false, params: [], desc: '所有字母变成小写' },
{ name: 'length', type: false, params: [], desc: '数据长度' },
{ name: 'substr', type: true, component: 'doubleInput', params: [], desc: '截取部分字符串' },
{ name: 'sha', type: true, component: 'select', params: ['sha1'], desc: 'sha加密' },
{ name: 'base64', type: false, params: [], desc: 'base64加密' },
{ name: 'unbase64', type: false, params: [], desc: 'base64解密' },
{ name: 'concat', type: true, component: 'input', params: [], desc: '连接字符串' },
{ name: 'lconcat', type: true, component: 'input', params: [], desc: '左连接' },
{ name: 'upper', type: false, desc: '所有字母变成大写' },
{ name: 'number', type: false, desc: '字符串转换为数字类型' }
];
class MethodsList extends Component {
static propTypes = {
show: PropTypes.bool,
click: PropTypes.func,
clickValue: PropTypes.string,
paramsInput: PropTypes.func,
clickIndex: PropTypes.number,
params: PropTypes.array
};
constructor(props) {
super(props);
this.state = {
list: METHODS_LIST,
moreFlag: true
};
}
showMore = () => {
this.setState({
moreFlag: false
});
};
componentDidMount() {
var index = _.findIndex(METHODS_LIST, { name: this.props.clickValue });
let moreFlag = index > 3 ? false : true;
this.setState({
moreFlag
});
}
inputComponent = props => {
let clickIndex = props.clickIndex;
let paramsIndex = props.paramsIndex;
let params = props.params;
return (
<Input
size="small"
placeholder="请输入参数"
value={params[0]}
onChange={e => this.handleParamsChange(e.target.value, clickIndex, paramsIndex, 0)}
/>
);
};
doubleInputComponent = props => {
let clickIndex = props.clickIndex;
let paramsIndex = props.paramsIndex;
let params = props.params;
return (
<div>
<Input
size="small"
placeholder="start"
value={params[0]}
onChange={e => this.handleParamsChange(e.target.value, clickIndex, paramsIndex, 0)}
/>
<Input
size="small"
placeholder="length"
value={params[1]}
onChange={e => this.handleParamsChange(e.target.value, clickIndex, paramsIndex, 1)}
/>
</div>
);
};
selectComponent = props => {
const subname = ['sha1', 'sha224', 'sha256', 'sha384', 'sha512'];
let clickIndex = props.clickIndex;
let paramsIndex = props.paramsIndex;
let params = props.params;
return (
<Select
value={params[0] || 'sha1'}
placeholder="请选择"
style={{ width: 150 }}
size="small"
onChange={e => this.handleParamsChange(e, clickIndex, paramsIndex, 0)}
>
{subname.map((item, index) => {
return (
<Option value={item} key={index}>
{item}
</Option>
);
})}
</Select>
);
};
// 处理参数输入
handleParamsChange(value, clickIndex, paramsIndex, index) {
let newList = deepEqual(this.state.list);
newList[paramsIndex].params[index] = value;
this.setState({
list: newList
});
this.props.paramsInput(value, clickIndex, index);
}
// 组件选择
handleComponent(item, clickIndex, index, params) {
let query = {
clickIndex: clickIndex,
paramsIndex: index,
params
};
switch (item.component) {
case 'select':
return this.selectComponent(query);
case 'input':
return this.inputComponent(query);
case 'doubleInput':
return this.doubleInputComponent(query);
default:
break;
}
}
render() {
const { list, moreFlag } = this.state;
const { click, clickValue, clickIndex, params } = this.props;
let showList = moreFlag ? list.slice(0, 4) : list;
return (
<div className="modal-postman-form-method">
<h3 className="methods-title title">方法</h3>
{showList.map((item, index) => {
return (
<Row
key={index}
type="flex"
align="middle"
className={'row methods-row ' + (item.name === clickValue ? 'checked' : '')}
onClick={() => click(item.name, showList[index].params)}
>
<Tooltip title={item.desc}>
<span>{item.name}</span>
</Tooltip>
<span className="input-component">
{item.type &&
this.handleComponent(
item,
clickIndex,
index,
item.name === clickValue ? params : []
)}
</span>
</Row>
);
})}
{moreFlag && (
<div className="show-more" onClick={this.showMore}>
<Icon type="down" />
<span style={{ paddingLeft: '4px' }}>更多</span>
</div>
)}
</div>
);
}
}
export default MethodsList;

View File

@ -0,0 +1,67 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Row, Input } from 'antd';
import constants from '../../constants/variable.js';
const wordList = constants.MOCK_SOURCE;
const Search = Input.Search;
class MockList extends Component {
static propTypes = {
click: PropTypes.func,
clickValue: PropTypes.string
};
constructor(props) {
super(props);
this.state = {
filter: '',
list: []
};
}
componentDidMount() {
this.setState({
list: wordList
});
}
onFilter = e => {
const list = wordList.filter(item => {
return item.mock.indexOf(e.target.value) !== -1;
});
this.setState({
filter: e.target.value,
list: list
});
};
render() {
const { list, filter } = this.state;
const { click, clickValue } = this.props;
return (
<div className="modal-postman-form-mock">
<Search
onChange={this.onFilter}
value={filter}
placeholder="搜索mock数据"
className="mock-search"
/>
{list.map((item, index) => {
return (
<Row
key={index}
type="flex"
align="middle"
className={'row ' + (item.mock === clickValue ? 'checked' : '')}
onClick={() => click(item.mock)}
>
<span>{item.mock}</span>
</Row>
);
})}
</div>
);
}
}
export default MockList;

View File

@ -0,0 +1,157 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Tree } from 'antd';
import { connect } from 'react-redux';
import { fetchVariableParamsList } from '../../reducer/modules/interfaceCol.js';
const TreeNode = Tree.TreeNode;
const CanSelectPathPrefix = 'CanSelectPath-';
function deleteLastObject(str) {
return str
.split('.')
.slice(0, -1)
.join('.');
}
function deleteLastArr(str) {
return str.replace(/\[.*?\]/g, '');
}
@connect(
state => {
return {
currColId: state.interfaceCol.currColId
};
},
{
fetchVariableParamsList
}
)
class VariablesSelect extends Component {
static propTypes = {
click: PropTypes.func,
currColId: PropTypes.number,
fetchVariableParamsList: PropTypes.func,
clickValue: PropTypes.string,
id: PropTypes.number
};
state = {
records: [],
expandedKeys: [],
selectedKeys: []
};
handleRecordsData(id) {
let newRecords = [];
this.id = id;
for (let i = 0; i < this.records.length; i++) {
if (this.records[i]._id === id) {
break;
}
newRecords.push(this.records[i]);
}
this.setState({
records: newRecords
});
}
async componentDidMount() {
const { currColId, fetchVariableParamsList, clickValue } = this.props;
let result = await fetchVariableParamsList(currColId);
let records = result.payload.data.data;
this.records = records.sort((a, b) => {
return a.index - b.index;
});
this.handleRecordsData(this.props.id);
if (clickValue) {
let isArrayParams = clickValue.lastIndexOf(']') === clickValue.length - 1;
let key = isArrayParams ? deleteLastArr(clickValue) : deleteLastObject(clickValue);
this.setState({
expandedKeys: [key],
selectedKeys: [CanSelectPathPrefix + clickValue]
});
// this.props.click(clickValue);
}
}
async componentWillReceiveProps(nextProps) {
if (this.records && nextProps.id && this.id !== nextProps.id) {
this.handleRecordsData(nextProps.id);
}
}
handleSelect = key => {
this.setState({
selectedKeys: [key]
});
if (key && key.indexOf(CanSelectPathPrefix) === 0) {
key = key.substr(CanSelectPathPrefix.length);
this.props.click(key);
} else {
this.setState({
expandedKeys: [key]
});
}
};
onExpand = keys => {
this.setState({ expandedKeys: keys });
};
render() {
const pathSelctByTree = (data, elementKeyPrefix = '$', deepLevel = 0) => {
let keys = Object.keys(data);
let TreeComponents = keys.map((key, index) => {
let item = data[key],
casename;
if (deepLevel === 0) {
elementKeyPrefix = '$';
elementKeyPrefix = elementKeyPrefix + '.' + item._id;
casename = item.casename;
item = {
params: item.params,
body: item.body
};
} else if (Array.isArray(data)) {
elementKeyPrefix =
index === 0
? elementKeyPrefix + '[' + key + ']'
: deleteLastArr(elementKeyPrefix) + '[' + key + ']';
} else {
elementKeyPrefix =
index === 0
? elementKeyPrefix + '.' + key
: deleteLastObject(elementKeyPrefix) + '.' + key;
}
if (item && typeof item === 'object') {
const isDisable = Array.isArray(item) && item.length === 0;
return (
<TreeNode key={elementKeyPrefix} disabled={isDisable} title={casename || key}>
{pathSelctByTree(item, elementKeyPrefix, deepLevel + 1)}
</TreeNode>
);
}
return <TreeNode key={CanSelectPathPrefix + elementKeyPrefix} title={key} />;
});
return TreeComponents;
};
return (
<div className="modal-postman-form-variable">
<Tree
expandedKeys={this.state.expandedKeys}
selectedKeys={this.state.selectedKeys}
onSelect={([key]) => this.handleSelect(key)}
onExpand={this.onExpand}
>
{pathSelctByTree(this.state.records)}
</Tree>
</div>
);
}
}
export default VariablesSelect;

View File

@ -0,0 +1,317 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './index.scss';
import { Alert, Modal, Row, Col, Icon, Collapse, Input, Tooltip } from 'antd';
import MockList from './MockList.js';
import MethodsList from './MethodsList.js';
import VariablesSelect from './VariablesSelect.js';
import { trim } from '../../common.js';
const { handleParamsValue } = require('common/utils.js');
const Panel = Collapse.Panel;
// 深拷贝
function deepEqual(state) {
return JSON.parse(JSON.stringify(state));
}
function closeRightTabsAndAddNewTab(arr, index, name, params) {
let newParamsList = [].concat(arr);
newParamsList.splice(index + 1, newParamsList.length - index);
newParamsList.push({
name: '',
params: []
});
let curParams = params || [];
let curname = name || '';
newParamsList[index] = {
...newParamsList[index],
name: curname,
params: curParams
};
return newParamsList;
}
class ModalPostman extends Component {
static propTypes = {
visible: PropTypes.bool,
handleCancel: PropTypes.func,
handleOk: PropTypes.func,
inputValue: PropTypes.any,
envType: PropTypes.string,
id: PropTypes.number
};
constructor(props) {
super(props);
this.state = {
methodsShow: false,
methodsShowMore: false,
methodsList: [],
constantInput: '',
activeKey: '1',
methodsParamsList: [
{
name: '',
params: [],
type: 'dataSource'
}
]
};
}
componentWillMount() {
let { inputValue } = this.props;
this.setState({
constantInput: inputValue
});
// this.props.inputValue && this.handleConstantsInput(this.props.inputValue, 0);
inputValue && this.handleInitList(inputValue);
}
handleInitList(val) {
val = val.replace(/^\{\{(.+)\}\}$/g, '$1');
let valArr = val.split('|');
if (valArr[0].indexOf('@') >= 0) {
this.setState({
activeKey: '2'
});
} else if (valArr[0].indexOf('$') >= 0) {
this.setState({
activeKey: '3'
});
}
let paramsList = [
{
name: trim(valArr[0]),
params: [],
type: 'dataSource'
}
];
for (let i = 1; i < valArr.length; i++) {
let nameArr = valArr[i].split(':');
let paramArr = nameArr[1] && nameArr[1].split(',');
paramArr =
paramArr &&
paramArr.map(item => {
return trim(item);
});
let item = {
name: trim(nameArr[0]),
params: paramArr || []
};
paramsList.push(item);
}
this.setState(
{
methodsParamsList: paramsList
},
() => {
this.mockClick(valArr.length)();
}
);
}
mockClick(index) {
return (curname, params) => {
let newParamsList = closeRightTabsAndAddNewTab(
this.state.methodsParamsList,
index,
curname,
params
);
this.setState({
methodsParamsList: newParamsList
});
};
}
// 处理常量输入
handleConstantsInput = val => {
val = val.replace(/^\{\{(.+)\}\}$/g, '$1');
this.setState({
constantInput: val
});
this.mockClick(0)(val);
};
handleParamsInput = (e, clickIndex, paramsIndex) => {
let newParamsList = deepEqual(this.state.methodsParamsList);
newParamsList[clickIndex].params[paramsIndex] = e;
this.setState({
methodsParamsList: newParamsList
});
};
// 方法
MethodsListSource = props => {
return (
<MethodsList
click={this.mockClick(props.index)}
clickValue={props.value}
params={props.params}
paramsInput={this.handleParamsInput}
clickIndex={props.index}
/>
);
};
// 处理表达式
handleValue(val) {
return handleParamsValue(val, {});
}
// 处理错误
handleError() {
return (
<Alert
message="请求“变量集”尚未运行,所以我们无法从其响应中提取的值。您可以在测试集合中测试这些变量。"
type="warning"
/>
);
}
// 初始化
setInit() {
let initParamsList = [
{
name: '',
params: [],
type: 'dataSource'
}
];
this.setState({
methodsParamsList: initParamsList
});
}
// 处理取消插入
handleCancel = () => {
this.setInit();
this.props.handleCancel();
};
// 处理插入
handleOk = installValue => {
this.props.handleOk(installValue);
this.setInit();
};
// 处理面板切换
handleCollapse = key => {
this.setState({
activeKey: key
});
};
render() {
const { visible, envType } = this.props;
const { methodsParamsList, constantInput } = this.state;
const outputParams = () => {
let str = '';
let length = methodsParamsList.length;
methodsParamsList.forEach((item, index) => {
let isShow = item.name && length - 2 !== index;
str += item.name;
item.params.forEach((item, index) => {
let isParams = index > 0;
str += isParams ? ' , ' : ' : ';
str += item;
});
str += isShow ? ' | ' : '';
});
return '{{ ' + str + ' }}';
};
return (
<Modal
title={
<p>
<Icon type="edit" /> 高级参数设置
</p>
}
visible={visible}
onOk={() => this.handleOk(outputParams())}
onCancel={this.handleCancel}
wrapClassName="modal-postman"
width={1024}
maskClosable={false}
okText="插入"
>
<Row className="modal-postman-form" type="flex">
{methodsParamsList.map((item, index) => {
return item.type === 'dataSource' ? (
<Col span={8} className="modal-postman-col" key={index}>
<Collapse
className="modal-postman-collapse"
activeKey={this.state.activeKey}
onChange={this.handleCollapse}
bordered={false}
accordion
>
<Panel header={<h3 className="mock-title">常量</h3>} key="1">
<Input
placeholder="基础参数值"
value={constantInput}
onChange={e => this.handleConstantsInput(e.target.value, index)}
/>
</Panel>
<Panel header={<h3 className="mock-title">mock数据</h3>} key="2">
<MockList click={this.mockClick(index)} clickValue={item.name} />
</Panel>
{envType === 'case' && (
<Panel
header={
<h3 className="mock-title">
变量&nbsp;<Tooltip
placement="top"
title="YApi 提供了强大的变量参数功能,你可以在测试的时候使用前面接口的 参数 或 返回值 作为 后面接口的参数,即使接口之间存在依赖,也可以轻松 一键测试~"
>
<Icon type="question-circle-o" />
</Tooltip>
</h3>
}
key="3"
>
<VariablesSelect
id={this.props.id}
click={this.mockClick(index)}
clickValue={item.name}
/>
</Panel>
)}
</Collapse>
</Col>
) : (
<Col span={8} className="modal-postman-col" key={index}>
<this.MethodsListSource index={index} value={item.name} params={item.params} />
</Col>
);
})}
</Row>
<Row className="modal-postman-expression">
<Col span={6}>
<h3 className="title">表达式</h3>
</Col>
<Col span={18}>
<span className="expression-item">{outputParams()}</span>
</Col>
</Row>
<Row className="modal-postman-preview">
<Col span={6}>
<h3 className="title">预览</h3>
</Col>
<Col span={18}>
<h3>{this.handleValue(outputParams()) || (outputParams() && this.handleError())}</h3>
</Col>
</Row>
</Modal>
);
}
}
export default ModalPostman;

View File

@ -0,0 +1,143 @@
.modal-postman {
.ant-modal-body {
padding: 0;
}
.ant-modal-footer {
background-color: #f5f5f5;
}
.modal-postman-form {
// padding: 0 16px;
max-height: 500px;
min-height: 400px;
overflow-y: scroll;
.ant-radio-group{
width:100%;
}
.mock-search {
padding-right: 8px;
margin-bottom: 16px;
}
.mock-checked{
color:#fff;
background-color:#2395f1;
width:100%
}
.row {
margin-bottom: 8px;
width: 100%;
padding: 8px 16px;
cursor: pointer;
}
.checked{
color:#fff;
background-color:#2395f1;
}
}
.modal-postman-expression, .modal-postman-preview {
border-top: 1px solid #e9e9e9;
padding: 8px 16px;
line-height: 38px;
}
.modal-postman-preview {
background-color: #f5f5f5;
}
.modal-postman-collapse{
.ant-collapse-item > .ant-collapse-header{
// padding-left: 20px;
// margin-left: 8px;
background-color: #f5f5f5;
}
.ant-collapse-item > .ant-collapse-header .arrow{
left: 14px;
font-size: 1.17em;
}
.ant-collapse-content > .ant-collapse-content-box{
padding-top: 8px;
}
.ant-collapse-content {
padding: 0 0 0 8px;
}
}
.title {
border-left: 3px solid #2395f1;
padding-left: 8px;
}
.modal-postman-form-mock, .modal-postman-form-variable{
max-height: 300px;
overflow-y: scroll;
}
.mock-title, .methods-title{
margin-bottom: 8px
}
.modal-postman-form-method{
padding-top: 16px;
margin-left: 8px;
max-height: 500px;
overflow: auto;
}
.methods-row{
position: relative;
// border-bottom: 1px solid #e9e9e9;
.ant-input-sm{
margin-top: 2px;
}
}
.methods-row:nth-child(5){
height: 67px;
}
.modal-postman-col{
border-right: 1px solid #e9e9e9;
}
.show-more{
color: #2395f1;
padding-left: 8px;
cursor:pointer;
}
.ant-row-flex {
flex-wrap: nowrap
}
.input-component {
position: absolute;
top: 2px;
right: 0;
width: 150px;
padding-top: 2px;
margin-right: 16px;
}
.modal-postman-expression{
.expression-item,.expression {
color: rgba(39,56,72,0.85);
font-size: 1.17em;
font-weight: 500;
line-height: 1.5em;
padding-right: 4px;
}
.expression-item{
color: #2395f1;
}
}
.modal-postman-preview{
h3{
word-wrap: break-word;
}
}
}

View File

@ -0,0 +1,51 @@
import React, { PureComponent as Component } from 'react';
import { Modal, Button } from 'antd';
import PropTypes from 'prop-types';
// 嵌入到 BrowserRouter 内部,覆盖掉默认的 window.confirm
// http://reacttraining.cn/web/api/BrowserRouter/getUserConfirmation-func
class MyPopConfirm extends Component {
constructor(props) {
super(props);
this.state = {
visible: true
};
}
static propTypes = {
msg: PropTypes.string,
callback: PropTypes.func
};
yes = () => {
this.props.callback(true);
this.setState({ visible: false });
}
no = () => {
this.props.callback(false);
this.setState({ visible: false });
}
componentWillReceiveProps() {
this.setState({ visible: true });
}
render() {
if (!this.state.visible) {
return null;
}
return (<Modal
title="你即将离开编辑页面"
visible={this.state.visible}
onCancel={this.no}
footer={[
<Button key="back" onClick={this.no}> </Button>,
<Button key="submit" onClick={this.yes}> </Button>
]}
>
<p>{this.props.msg}</p>
</Modal>);
}
}
export default MyPopConfirm;

View File

@ -0,0 +1,51 @@
import React, { Component } from 'react';
import axios from 'axios';
import { Alert, message } from 'antd';
export default class Notify extends Component {
constructor(props) {
super(props);
this.state = {
newVersion: process.env.version,
version: process.env.version
};
}
componentDidMount() {
const versions = 'https://www.fastmock.site/mock/1529fa78fa4c4880ad153d115084a940/yapi/versions';
axios.get(versions).then(req => {
if (req.status === 200) {
this.setState({ newVersion: req.data.data[0] });
} else {
message.error('无法获取新版本信息!');
}
});
}
render() {
const isShow = this.state.newVersion !== this.state.version;
return (
<div>
{isShow && (
<Alert
message={
<div>
当前版本是{this.state.version}&nbsp;&nbsp;可升级到: {this.state.newVersion}
&nbsp;&nbsp;&nbsp;
<a
target="view_window"
href="https://github.com/YMFE/yapi/blob/master/CHANGELOG.md"
>
版本详情
</a>
</div>
}
banner
closable
type="info"
/>
)}
</div>
);
}
}

View File

@ -0,0 +1,61 @@
import React from 'react';
import { Alert } from 'antd';
import PropTypes from 'prop-types';
exports.initCrossRequest = function (fn) {
let startTime = 0;
let _crossRequest = setInterval(() => {
startTime += 500;
if (startTime > 5000) {
clearInterval(_crossRequest);
}
if (window.crossRequest) {
clearInterval(_crossRequest);
fn(true);
} else {
fn(false);
}
}, 500);
return _crossRequest;
};
CheckCrossInstall.propTypes = {
hasPlugin: PropTypes.bool
};
function CheckCrossInstall(props) {
const hasPlugin = props.hasPlugin;
return (
<div className={hasPlugin ? null : 'has-plugin'}>
{hasPlugin ? (
''
) : (
<Alert
message={
<div>
重要当前的接口测试服务需安装免费测试增强插件,仅支持 chrome
浏览器选择下面任意一种安装方式
{/* <div>
<a
target="blank"
href="https://chrome.google.com/webstore/detail/cross-request/cmnlfmgbjmaciiopcgodlhpiklaghbok?hl=en-US"
>
[Google 商店获取需翻墙]
</a>
</div> */}
<div>
<a target="blank" href="https://juejin.im/post/5e3bbd986fb9a07ce152b53d">
{' '}
[谷歌请求插件详细安装教程]{' '}
</a>
</div>
</div>
}
type="warning"
/>
)}
</div>
);
}
export default CheckCrossInstall;

1051
vendors/client/components/Postman/Postman.js vendored Executable file

File diff suppressed because it is too large Load Diff

189
vendors/client/components/Postman/Postman.scss vendored Executable file
View File

@ -0,0 +1,189 @@
@import '../../styles/mixin.scss';
.postman {
.adv-button {
margin-bottom: 8px;
}
.pretty-editor {
border: 1px solid #d9d9d9;
border-radius: 4px;
min-height: 200px;
}
.pretty-editor-body {
border: 1px solid #d9d9d9;
border-radius: 4px;
min-height: 300px;
min-width: 100%;
}
.pretty-editor-header {
border: 1px solid #d9d9d9;
border-radius: 4px;
min-height: 300px;
}
.interface-test {
padding: .24rem;
.ant-checkbox-wrapper{
display: flex;
}
}
.insert-code{
margin-right: 20px;
.code-item{
border: 1px solid #dbd9d9;
padding:3px;
line-height: 30px;
margin-bottom: 5px;
}
}
.case-script{
min-height: 500px;
margin: 10px;
}
.response-tab{
margin-top: 20px;
margin-bottom: 20px;
.ant-tabs-nav{
background: #f7f7f7;
border-radius: 0 0 4px 4px;
border: 1px solid #d9d9d9;
width: 100%;
}
.header, .body{
margin-bottom: 10px;
}
.header {
padding-left: 10px;
padding-right: 5px;
}
.body {
padding-left: 5px;
padding-right: 10px;
}
.response-test{
min-height: 400px;
}
}
.ant-spin-blur {
.res-code.success {
background-color: transparent;
}
.res-code.fail {
background-color: transparent;
}
}
.res-code {
padding: .08rem .28rem;
color: #fff;
margin-left: -.1rem;
margin-right: -.28rem;
transition: all .2s;
position: relative;
border-radius: 2px;
}
.res-code.success {
background-color: $color-antd-green;
}
.res-code.fail {
background-color: $color-antd-red;
}
// 容器左侧是header 右侧是body
.container-header-body {
display: flex;
padding-bottom: .36rem;
.header, .body {
flex: 1 0 300px;
.pretty-editor-header, .pretty-editor-body {
height: 100%;
}
.postman .pretty-editor-body {
min-height: 200px;
}
.ace_print-margin {
display: none;
}
}
.header {
max-width: 400px;
}
.container-title {
display: flex;
justify-content: space-between;
padding: .08rem 0;
}
.resizer {
flex: 0 0 21px;
position: relative;
&:after {
content: '';
display: block;
width: 1px;
height: 100%;
background-color: #acaaaa;
opacity: .8;
position: absolute;
left: 50%;
}
}
// res body 无返回json时显示text信息
.res-body-text {
height: 100%;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 8px;
}
}
.has-plugin, .req-part, .resp-part {
margin-bottom: 16px;
}
.url {
display: flex;
margin: 24px 10px;
}
.key-value-wrap {
display: flex;
align-items: center;
margin: 0 0 5px 0;
.key {
flex-basis: 220px;
}
.value {
flex-grow: 1;
}
.eq-symbol {
margin: 0 5px;
}
.params-enable{
width: 24px;
}
}
.icon-btn {
cursor: pointer;
margin-left: 6px;
}
.icon-btn:hover {
color: #2395f1;
}
}
.env-modal{
.ant-modal-body{
padding: 0;
}
}

View File

@ -0,0 +1,179 @@
import './ProjectCard.scss';
import React, { PureComponent as Component } from 'react';
import { Card, Icon, Tooltip, Modal, Alert, Input, message } from 'antd';
import { connect } from 'react-redux';
import { delFollow, addFollow } from '../../reducer/modules/follow';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import { debounce } from '../../common';
import constants from '../../constants/variable.js';
import produce from 'immer';
import { getProject, checkProjectName, copyProjectMsg } from '../../reducer/modules/project';
import { trim } from '../../common.js';
const confirm = Modal.confirm;
@connect(
state => {
return {
uid: state.user.uid,
currPage: state.project.currPage
};
},
{
delFollow,
addFollow,
getProject,
checkProjectName,
copyProjectMsg
}
)
@withRouter
class ProjectCard extends Component {
constructor(props) {
super(props);
this.add = debounce(this.add, 400);
this.del = debounce(this.del, 400);
}
static propTypes = {
projectData: PropTypes.object,
uid: PropTypes.number,
inFollowPage: PropTypes.bool,
callbackResult: PropTypes.func,
history: PropTypes.object,
delFollow: PropTypes.func,
addFollow: PropTypes.func,
isShow: PropTypes.bool,
getProject: PropTypes.func,
checkProjectName: PropTypes.func,
copyProjectMsg: PropTypes.func,
currPage: PropTypes.number
};
copy = async projectName => {
const id = this.props.projectData._id;
let projectData = await this.props.getProject(id);
let data = projectData.payload.data.data;
let newData = produce(data, draftData => {
draftData.preName = draftData.name;
draftData.name = projectName;
});
await this.props.copyProjectMsg(newData);
message.success('项目复制成功');
this.props.callbackResult();
};
// 复制项目的二次确认
showConfirm = () => {
const that = this;
confirm({
title: '确认复制 ' + that.props.projectData.name + ' 项目吗?',
okText: '确认',
cancelText: '取消',
content: (
<div style={{ marginTop: '10px', fontSize: '13px', lineHeight: '25px' }}>
<Alert
message={`该操作将会复制 ${
that.props.projectData.name
} 下的所有接口集合但不包括测试集合中的接口`}
type="info"
/>
<div style={{ marginTop: '16px' }}>
<p>
<b>项目名称:</b>
</p>
<Input id="project_name" placeholder="项目名称" />
</div>
</div>
),
async onOk() {
const projectName = trim(document.getElementById('project_name').value);
// 查询项目名称是否重复
const group_id = that.props.projectData.group_id;
await that.props.checkProjectName(projectName, group_id);
that.copy(projectName);
},
iconType: 'copy',
onCancel() {}
});
};
del = () => {
const id = this.props.projectData.projectid || this.props.projectData._id;
this.props.delFollow(id).then(res => {
if (res.payload.data.errcode === 0) {
this.props.callbackResult();
// message.success('已取消关注!'); // 星号已做出反馈 无需重复提醒用户
}
});
};
add = () => {
const { uid, projectData } = this.props;
const param = {
uid,
projectid: projectData._id,
projectname: projectData.name,
icon: projectData.icon || constants.PROJECT_ICON[0],
color: projectData.color || constants.PROJECT_COLOR.blue
};
this.props.addFollow(param).then(res => {
if (res.payload.data.errcode === 0) {
this.props.callbackResult();
// message.success('已添加关注!'); // 星号已做出反馈 无需重复提醒用户
}
});
};
render() {
const { projectData, inFollowPage, isShow } = this.props;
return (
<div className="card-container">
<Card
bordered={false}
className="m-card"
onClick={() =>
this.props.history.push('/project/' + (projectData.projectid || projectData._id))
}
>
<Icon
type={projectData.icon || 'star-o'}
className="ui-logo"
style={{
backgroundColor:
constants.PROJECT_COLOR[projectData.color] || constants.PROJECT_COLOR.blue
}}
/>
<h4 className="ui-title">{projectData.name || projectData.projectname}</h4>
</Card>
<div
className="card-btns"
onClick={projectData.follow || inFollowPage ? this.del : this.add}
>
<Tooltip
placement="rightTop"
title={projectData.follow || inFollowPage ? '取消关注' : '添加关注'}
>
<Icon
type={projectData.follow || inFollowPage ? 'star' : 'star-o'}
className={'icon ' + (projectData.follow || inFollowPage ? 'active' : '')}
/>
</Tooltip>
</div>
{isShow && (
<div className="copy-btns" onClick={this.showConfirm}>
<Tooltip placement="rightTop" title="复制项目">
<Icon type="copy" className="icon" />
</Tooltip>
</div>
)}
</div>
);
}
}
export default ProjectCard;

View File

@ -0,0 +1,163 @@
@import '../../styles/mixin.scss';
.card-container {
position: relative;
user-select: none;
transition: all .2s;
.m-card, .card-btns {
transform: translateY(0);
transition: all .2s;
}
&:hover {
.m-card, .card-btns , .copy-btns {
transform: translateY(-4px);
}
.m-card .ant-card-body {
background-color: $color-bg-gray;
box-shadow: 0 4px 8px rgba(50, 50, 93, 0.11), 0 4px 6px rgba(0, 0, 0, 0.08);
}
.card-btns .icon {
color: rgba(39, 56, 72, 0.85);
}
.copy-btns .icon {
color: #2395f1
}
.card-btns .icon.active , .copy-btns .icon.active {
color: #fac200;
}
}
&:active {
.m-card, .card-btns, .copy-btns {
transform: translateY(4px);
}
}
// 覆盖 card 组件 hover 状态的默认阴影样式
.ant-card:not(.ant-card-no-hovering):hover {
box-shadow: none;
}
// 卡片右上角按钮
.card-btns {
position: absolute;
top: 0;
right: 0;
width: .48rem;
height: .48rem;
// background: linear-gradient(225deg, #ccc, #ccc 50%, transparent 0);
border-top-right-radius: 4px;
.icon {
cursor: pointer;
font-size: .16rem;
padding: .06rem;
position: absolute;
right: 0;
top: 0;
color: #fff;
}
.icon.active {
color: #fff;
}
}
// 卡片昨上角按钮
.copy-btns {
position: absolute;
top: 0;
left: 0;
width: .48rem;
height: .48rem;
// background: linear-gradient(225deg, #ccc, #ccc 50%, transparent 0);
border-top-right-radius: 4px;
.icon {
cursor: pointer;
font-size: .16rem;
padding: .06rem;
position: absolute;
right: 0;
top: 3px;
color: #fff;
}
.icon.active {
color: #fff;
}
}
}
.m-card {
cursor: pointer;
text-align: center;
margin-bottom: .16rem;
transition: all .4s;
position: relative;
.ant-card-body {
background-color: transparent;
border-radius: 4px;
padding-top: .24rem + .16rem + 1rem;
box-shadow: 0 4px 6px rgba(255,255,255,.11), 0 1px 3px rgba(255,255,255,.08);
// box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
transition: all .2s;
}
.ui-logo {
width: 1rem;
height: 1rem;
border-radius: 50%;
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, .24rem);
font-size: .5rem;
color: #fff;
background-color: #2395f1;
line-height: 1rem;
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
}
.ui-title {
font-size: .19rem;
font-weight: normal;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
.m-card-body {
.icon {
font-size: .8rem;
}
.name {
font-size: .18rem;
margin-top: .16rem;
}
}
}
@media (max-width: 768px) {
.m-card {
.ui-logo {
width: .6rem;
height: .6rem;
line-height: .6rem;
font-size: .3rem;
transform: translate(-50%, 0.08rem);
}
.ant-card-body {
padding-top: .08rem + .08rem + .6rem;
padding-bottom: .08rem;
}
}
}
@media (min-width: 768px) and (max-width: 992px) {
.m-card {
.ui-logo {
width: .8rem;
height: .8rem;
line-height: .8rem;
font-size: .4rem;
transform: translate(-50%, 0.16rem);
}
.ant-card-body {
padding-top: .16rem + .16rem + .8rem;
padding-bottom: .16rem;
}
}
}

View File

@ -0,0 +1,128 @@
import React, { Component } from 'react';
import { Table } from 'antd';
import json5 from 'json5';
import PropTypes from 'prop-types';
import { schemaTransformToTable } from '../../../common/schema-transformTo-table.js';
import _ from 'underscore';
import './index.scss';
const messageMap = {
desc: '备注',
default: '实例',
maximum: '最大值',
minimum: '最小值',
maxItems: '最大数量',
minItems: '最小数量',
maxLength: '最大长度',
minLength: '最小长度',
enum: '枚举',
enumDesc: '枚举备注',
uniqueItems: '元素是否都不同',
itemType: 'item 类型',
format: 'format',
itemFormat: 'format',
mock: 'mock'
};
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100,
render: (text, item) => {
// console.log('text',item.sub);
return text === 'array' ? (
<span>{item.sub ? item.sub.itemType || '' : 'array'} []</span>
) : (
<span>{text}</span>
);
}
},
{
title: '是否必须',
dataIndex: 'required',
key: 'required',
width: 80,
render: text => {
return <div>{text ? '必须' : '非必须'}</div>;
}
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 80,
render: text => {
return <div>{_.isBoolean(text) ? text + '' : text}</div>;
}
},
{
title: '备注',
dataIndex: 'desc',
key: 'desc',
render: (text, item) => {
return _.isUndefined(item.childrenDesc) ? (
<span className="table-desc">{text}</span>
) : (
<span className="table-desc">{item.childrenDesc}</span>
);
}
},
{
title: '其他信息',
dataIndex: 'sub',
key: 'sub',
width: 180,
render: (text, record) => {
let result = text || record;
return Object.keys(result).map((item, index) => {
let name = messageMap[item];
let value = result[item];
let isShow = !_.isUndefined(result[item]) && !_.isUndefined(name);
return (
isShow && (
<p key={index}>
<span style={{ fontWeight: '700' }}>{name}: </span>
<span>{value.toString()}</span>
</p>
)
);
});
}
}
];
class SchemaTable extends Component {
static propTypes = {
dataSource: PropTypes.string
};
constructor(props) {
super(props);
}
render() {
let product;
try {
product = json5.parse(this.props.dataSource);
} catch (e) {
product = null;
}
if (!product) {
return null;
}
let data = schemaTransformToTable(product);
data = _.isArray(data) ? data : [];
return <Table bordered size="small" pagination={false} dataSource={data} columns={columns} />;
}
}
export default SchemaTable;

View File

@ -0,0 +1,3 @@
.table-desc {
white-space: pre-wrap;
}

43
vendors/client/components/Subnav/Subnav.js vendored Executable file
View File

@ -0,0 +1,43 @@
import './Subnav.scss';
import React, { PureComponent as Component } from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Menu } from 'antd';
class Subnav extends Component {
constructor(props) {
super(props);
}
static propTypes = {
data: PropTypes.array,
default: PropTypes.string
};
render() {
return (
<div className="m-subnav">
<Menu
onClick={this.handleClick}
selectedKeys={[this.props.default]}
mode="horizontal"
className="g-row m-subnav-menu"
>
{this.props.data.map((item, index) => {
// 若导航标题为两个字,则自动在中间加个空格
if (item.name.length === 2) {
item.name = item.name[0] + ' ' + item.name[1];
}
return (
<Menu.Item className="item" key={item.name.replace(' ', '')}>
<Link to={item.path}>{this.props.data[index].name}</Link>
</Menu.Item>
);
})}
</Menu>
</div>
);
}
}
export default Subnav;

21
vendors/client/components/Subnav/Subnav.scss vendored Executable file
View File

@ -0,0 +1,21 @@
@import '../../styles/common.scss';
.m-subnav {
margin-bottom: .24rem;
background-color: #fff;
box-shadow: 0 0 .04rem rgba(0, 0, 0, .08);
font-size: .14rem;
border: none;
.ant-menu {
font-size: unset;
}
.m-subnav-menu {
border: none;
padding: 0 .24rem;
.item {
line-height: .54rem;
padding: 0 .36rem;
font-weight: normal;
}
}
}

View File

@ -0,0 +1,287 @@
import React, { PureComponent as Component } from 'react';
import { Timeline, Spin, Row, Col, Tag, Avatar, Button, Modal, AutoComplete } from 'antd';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { formatTime } from '../../common.js';
import showDiffMsg from '../../../common/diff-view.js';
import variable from '../../constants/variable';
import { Link } from 'react-router-dom';
import { fetchNewsData, fetchMoreNews } from '../../reducer/modules/news.js';
import { fetchInterfaceList } from '../../reducer/modules/interface.js';
import ErrMsg from '../ErrMsg/ErrMsg.js';
const jsondiffpatch = require('jsondiffpatch/dist/jsondiffpatch.umd.js');
const formattersHtml = jsondiffpatch.formatters.html;
import 'jsondiffpatch/dist/formatters-styles/annotated.css';
import 'jsondiffpatch/dist/formatters-styles/html.css';
import './TimeLine.scss';
import { timeago } from '../../../common/utils.js';
// const Option = AutoComplete.Option;
const { Option, OptGroup } = AutoComplete;
const AddDiffView = props => {
const { title, content, className } = props;
if (!content) {
return null;
}
return (
<div className={className}>
<h3 className="title">{title}</h3>
<div dangerouslySetInnerHTML={{ __html: content }} />
</div>
);
};
AddDiffView.propTypes = {
title: PropTypes.string,
content: PropTypes.string,
className: PropTypes.string
};
// timeago(new Date().getTime() - 40);
@connect(
state => {
return {
newsData: state.news.newsData,
curpage: state.news.curpage,
curUid: state.user.uid
};
},
{
fetchNewsData,
fetchMoreNews,
fetchInterfaceList
}
)
class TimeTree extends Component {
static propTypes = {
newsData: PropTypes.object,
fetchNewsData: PropTypes.func,
fetchMoreNews: PropTypes.func,
setLoading: PropTypes.func,
loading: PropTypes.bool,
curpage: PropTypes.number,
typeid: PropTypes.number,
curUid: PropTypes.number,
type: PropTypes.string,
fetchInterfaceList: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
bidden: '',
loading: false,
visible: false,
curDiffData: {},
apiList: []
};
this.curSelectValue = '';
}
getMore() {
const that = this;
if (this.props.curpage <= this.props.newsData.total) {
this.setState({ loading: true });
this.props
.fetchMoreNews(
this.props.typeid,
this.props.type,
this.props.curpage + 1,
10,
this.curSelectValue
)
.then(function() {
that.setState({ loading: false });
if (that.props.newsData.total === that.props.curpage) {
that.setState({ bidden: 'logbidden' });
}
});
}
}
handleCancel = () => {
this.setState({
visible: false
});
};
componentWillMount() {
this.props.fetchNewsData(this.props.typeid, this.props.type, 1, 10);
if (this.props.type === 'project') {
this.getApiList();
}
}
openDiff = data => {
this.setState({
curDiffData: data,
visible: true
});
};
async getApiList() {
let result = await this.props.fetchInterfaceList({
project_id: this.props.typeid,
limit: 'all'
});
this.setState({
apiList: result.payload.data.data.list
});
}
handleSelectApi = selectValue => {
this.curSelectValue = selectValue;
this.props.fetchNewsData(this.props.typeid, this.props.type, 1, 10, selectValue);
};
render() {
let data = this.props.newsData ? this.props.newsData.list : [];
const curDiffData = this.state.curDiffData;
let logType = {
project: '项目',
group: '分组',
interface: '接口',
interface_col: '接口集',
user: '用户',
other: '其他'
};
const children = this.state.apiList.map(item => {
let methodColor = variable.METHOD_COLOR[item.method ? item.method.toLowerCase() : 'get'];
return (
<Option title={item.title} value={item._id + ''} path={item.path} key={item._id}>
{item.title}{' '}
<Tag
style={{ color: methodColor ? methodColor.color : '#cfefdf', backgroundColor: methodColor ? methodColor.bac : '#00a854', border: 'unset' }}
>
{item.method}
</Tag>
</Option>
);
});
children.unshift(
<Option value="" key="all">
选择全部
</Option>
);
if (data && data.length) {
data = data.map((item, i) => {
let interfaceDiff = false;
// 去掉了 && item.data.interface_id
if (item.data && typeof item.data === 'object') {
interfaceDiff = true;
}
return (
<Timeline.Item
dot={
<Link to={`/user/profile/${item.uid}`}>
<Avatar src={`/api/user/avatar?uid=${item.uid}`} />
</Link>
}
key={i}
>
<div className="logMesHeade">
<span className="logoTimeago">{timeago(item.add_time)}</span>
{/*<span className="logusername"><Link to={`/user/profile/${item.uid}`}><Icon type="user" />{item.username}</Link></span>*/}
<span className="logtype">{logType[item.type]}动态</span>
<span className="logtime">{formatTime(item.add_time)}</span>
</div>
<span className="logcontent" dangerouslySetInnerHTML={{ __html: item.content }} />
<div style={{ padding: '10px 0 0 10px' }}>
{interfaceDiff && <Button onClick={() => this.openDiff(item.data)}>改动详情</Button>}
</div>
</Timeline.Item>
);
});
} else {
data = '';
}
let pending =
this.props.newsData.total <= this.props.curpage ? (
<a className="logbidden">以上为全部内容</a>
) : (
<a className="loggetMore" onClick={this.getMore.bind(this)}>
查看更多
</a>
);
if (this.state.loading) {
pending = <Spin />;
}
let diffView = showDiffMsg(jsondiffpatch, formattersHtml, curDiffData);
return (
<section className="news-timeline">
<Modal
style={{ minWidth: '800px' }}
title="Api 改动日志"
visible={this.state.visible}
footer={null}
onCancel={this.handleCancel}
>
<i> 绿色代表新增内容红色代表删除内容</i>
<div className="project-interface-change-content">
{diffView.map((item, index) => {
return (
<AddDiffView
className="item-content"
title={item.title}
key={index}
content={item.content}
/>
);
})}
{diffView.length === 0 && <ErrMsg type="noChange" />}
</div>
</Modal>
{this.props.type === 'project' && (
<Row className="news-search">
<Col span="3">选择查询的 Api</Col>
<Col span="10">
<AutoComplete
onSelect={this.handleSelectApi}
style={{ width: '100%' }}
placeholder="Select Api"
optionLabelProp="title"
filterOption={(inputValue, options) => {
if (options.props.value == '') return true;
if (
options.props.path.indexOf(inputValue) !== -1 ||
options.props.title.indexOf(inputValue) !== -1
) {
return true;
}
return false;
}}
>
{/* {children} */}
<OptGroup label="other">
<Option value="wiki" path="" title="wiki">
wiki
</Option>
</OptGroup>
<OptGroup label="api">{children}</OptGroup>
</AutoComplete>
</Col>
</Row>
)}
{data ? (
<Timeline className="news-content" pending={pending}>
{data}
</Timeline>
) : (
<ErrMsg type="noData" />
)}
</section>
);
}
}
export default TimeTree;

View File

@ -0,0 +1,206 @@
.project-interface-change-content{
min-height: 350px;
max-height: 1000px;
min-width: 784px;
overflow-y: scroll;
.item-content{
.title{
margin: 10px 0;
}
.content{
}
}
}
.news-box {
display: -webkit-box;
-webkit-box-flex: 1;
margin: 0px auto 0 auto;
font-size: 0.14rem;
background: #FFF;
display: block;
min-height: 550px;
border-radius: 4px;
.news-timeline{
padding-left: 125px;
color: #6b6c6d;
.news-search{
height:35px;
line-height: 30px;
margin-top: 10px;
}
.news-content{
margin-top: 20px;
}
.ant-timeline-item{
min-height: 60px;
.ant-timeline-item-head-custom {
padding: 0;
width: 0;
left: -14px;
}
.ant-timeline-item-head{
// width: 40px;
// height: 40px;
// left: -13px;
top: 13px;
// // border-color:#e1e3e4;
// border:2px solid #e1e3e4;
// border-radius: 50%;
.anticon{
display: none;
}
}
.ant-timeline-item-tail{
// top: 30px;
}
.ant-avatar {
border: 2px solid #e1e3e4;
box-sizing: content-box;
border-radius: 50%;
width: 36px;
height: 36px;
}
}
.ant-avatar{
// border:2px solid gray;
}
.logusername{
color: #4eaef3;
padding: 0px 16px 0px 8px;
cursor: pointer;
}
.logtype{
padding-right: 16px;
}
.logtime{
padding-right: 16px;
}
.logcontent{
display: block;
padding-left: 8px;
line-height: 24px;
}
.logoTimeago{
position: absolute;
left: -80px;
top: 5px;
color: #c0c1c1;
}
.logbidden{
color: #c0c1c1;
cursor: default;
padding: 8px !important;
}
.loggetMore{
line-height: 30px;
color: #4eaef3;
}
.ant-timeline-item{
&:after{
content: "";
width: 0px;
height: 0px;
display: block;
clear: both;
}
.ant-timeline-item-content{
background-color: #fafafa;
float: left;
width: auto;
margin-left: 40px;
padding: 0px;
padding-bottom: 10px;
// min-width: 300px;
// max-width: 600px;
width: 625px;
border-radius: 8px;
.logMesHeade{
padding: 8px 8px 8px 8px;
line-height: 24px;
background-color: #eceef1;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.logoTimeago{
left: -120px;
}
.logcontent{
// text-indent: 2em;
line-height: 1.5em;
margin-top: 16px;
padding: 0px 16px;
}
}
}
.ant-timeline-item-pending{
padding: 0px;
.ant-timeline-item-content{
padding: 0px;
width: auto;
margin-top: 16px;
.loggetMore{
margin: 0px;
padding: 8px;
}
}
}
}
.logHead{
height: 80px;
width: 100%;
border-bottom: 1px solid #e9e9e9;
padding: 24px 0px;
overflow: hidden;
.breadcrumb-container{
float: left;
min-width:100px;
}
.projectDes{
color: #7b7b7b;
font-size: 25px;
float: left;
line-height: 0.9em;
}
.Mockurl{
width: 600px !important;
float: right;
color: #7b7b7b;
>span{
float: left;
line-height: 30px;
}
p{
width: 65%;
display: inline-block;
position: relative;
padding: 4px 7px;
height: 28px;
cursor: text;
font-size: 13px;
color: rgba(0,0,0,.65);
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: 4px;
-webkit-transition: all .3s;
transition: all .3s;
overflow-x:auto;
}
button{
float: right;
}
}
}
}

View File

@ -0,0 +1,122 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { Select } from 'antd';
import axios from 'axios';
const Option = Select.Option;
/**
* 用户名输入框自动完成组件
*
* @component UsernameAutoComplete
* @examplelanguage js
*
* * 用户名输入框自动完成组件
* * 用户名输入框自动完成组件
*
*s
*/
/**
* 获取自动输入的用户信息
*
* 获取子组件state
* @property callbackState
* @type function
* @description 类型提示支持数组传值也支持用函数格式化字符串函数有两个参数(scale, index)
* 受控属性滑块滑到某一刻度时所展示的刻度文本信息如果不需要标签请将该属性设置为 [] 空列表来覆盖默认转换函数
* @returns {object} {uid: xxx, username: xxx}
* @examplelanguage js
* @example
* onUserSelect(childState) {
* this.setState({
* uid: childState.uid,
* username: childState.username
* })
* }
*
*/
class UsernameAutoComplete extends Component {
constructor(props) {
super(props);
// this.lastFetchId = 0;
// this.fetchUser = debounce(this.fetchUser, 800);
}
state = {
dataSource: [],
fetching: false
};
static propTypes = {
callbackState: PropTypes.func
};
// 搜索回调
handleSearch = value => {
const params = { q: value };
// this.lastFetchId += 1;
// const fetchId = this.lastFetchId;
this.setState({ fetching: true });
axios.get('/api/user/search', { params }).then(data => {
// if (fetchId !== this.lastFetchId) { // for fetch callback order
// return;
// }
const userList = [];
data = data.data.data;
if (data) {
data.forEach(v =>
userList.push({
username: v.username,
id: v.uid
})
);
// 取回搜索值后,设置 dataSource
this.setState({
dataSource: userList
});
}
});
};
// 选中候选词时
handleChange = value => {
this.setState({
dataSource: [],
// value,
fetching: false
});
this.props.callbackState(value);
};
render() {
let { dataSource, fetching } = this.state;
const children = dataSource.map((item, index) => (
<Option key={index} value={'' + item.id}>
{item.username}
</Option>
));
// if (!children.length) {
// fetching = false;
// }
return (
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="请输入用户名"
filterOption={false}
optionLabelProp="children"
notFoundContent={fetching ? <span style={{ color: 'red' }}> 当前用户不存在</span> : null}
onSearch={this.handleSearch}
onChange={this.handleChange}
>
{children}
</Select>
);
}
}
export default UsernameAutoComplete;

10
vendors/client/components/index.js vendored Executable file
View File

@ -0,0 +1,10 @@
import Breadcrumb from './Breadcrumb/Breadcrumb.js';
import Footer from './Footer/Footer.js';
import Header from './Header/Header.js';
import Intro from './Intro/Intro.js';
import Loading from './Loading/Loading.js';
import ProjectCard from './ProjectCard/ProjectCard.js';
import Subnav from './Subnav/Subnav.js';
import Postman from './Postman/Postman';
export { Breadcrumb, Footer, Header, Intro, Loading, ProjectCard, Subnav, Postman };

173
vendors/client/constants/variable.js vendored Executable file
View File

@ -0,0 +1,173 @@
module.exports = {
PAGE_LIMIT: 10, // 默认每页展示10条数据
NAME_LIMIT: 100, // 限制名称的字符长度(中文算两个长度)
HTTP_METHOD: {
'GET': {
request_body: false,
default_tab: 'query'
},
'POST': {
request_body: true,
default_tab: 'body'
},
'PUT': {
request_body: true,
default_tab: 'body'
},
'DELETE': {
request_body: true,
default_tab: 'body'
},
'HEAD': {
request_body: false,
default_tab: 'query'
},
'OPTIONS': {
request_body: false,
default_tab: 'query'
},
'PATCH': {
request_body: true,
default_tab: 'body'
}
},
PROJECT_COLOR: {
blue: '#2395f1',
green: '#00a854',
yellow: '#ffbf00',
red: '#f56a00',
pink: '#f5317f',
cyan: '#00a2ae',
gray: '#bfbfbf',
purple: '#7265e6'
},
PROJECT_ICON: [
'code-o',
'swap',
'clock-circle-o',
'unlock',
'calendar',
'play-circle-o',
'file-text',
'desktop',
'hdd',
'appstore-o',
'line-chart',
'mail',
'mobile',
'notification',
'picture',
'poweroff',
'search',
'setting',
'share-alt',
'shopping-cart',
'tag-o',
'video-camera',
'cloud-o',
'star-o',
'environment-o',
'camera-o',
'team',
'customer-service',
'pay-circle-o',
'rocket',
'database',
'tool',
'wifi',
'idcard',
'medicine-box',
'coffee',
'safety',
'global',
'api',
'fork',
'android-o',
'apple-o'
],
HTTP_REQUEST_HEADER: ["Accept", "Accept-Charset", "Accept-Encoding", "Accept-Language", "Accept-Datetime", "Authorization", "Cache-Control", "Connection", "Cookie", "Content-Disposition", "Content-Length", "Content-MD5", "Content-Type", "Date", "Expect", "From", "Host", "If-Match", "If-Modified-Since", "If-None-Match", "If-Range", "If-Unmodified-Since", "Max-Forwards", "Origin", "Pragma", "Proxy-Authorization", "Range", "Referer", "TE", "User-Agent", "Upgrade", "Via", "Warning", "X-Requested-With", "DNT", "X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Proto", "Front-End-Https", "X-Http-Method-Override", "X-ATT-DeviceId", "X-Wap-Profile", "Proxy-Connection", "X-UIDH", "X-Csrf-Token"],
METHOD_COLOR: {
post: {
bac: "#d2eafb",
color: "#108ee9"
},
get: {
bac: "#cfefdf",
color: "#00a854"
},
put: {
bac: "#fff3cf",
color: "#ffbf00"
},
delete: {
bac: "#fcdbd9",
color: "#f04134"
},
head: {
bac: "#fff3cf",
color: "#ffbf00"
},
patch: {
bac: "#fff3cf",
color: "#ffbf00"
},
options: {
bac: "#fff3cf",
color: "#ffbf00"
}
},
MOCK_SOURCE: [
{ name: '字符串', mock: '@string' },
{ name: '自然数', mock: '@natural' },
{ name: '浮点数', mock: '@float' },
{ name: '字符', mock: '@character' },
{ name: '布尔', mock: '@boolean' },
{ name: 'url', mock: '@url' },
{ name: '域名', mock: '@domain' },
{ name: 'ip地址', mock: '@ip' },
{ name: 'id', mock: '@id' },
{ name: 'guid', mock: '@guid' },
{ name: '当前时间', mock: '@now' },
{ name: '时间戳', mock: '@timestamp'},
{ name: '日期', mock: '@date' },
{ name: '时间', mock: '@time' },
{ name: '日期时间', mock: '@datetime' },
{ name: '图片连接', mock: '@image' },
{ name: '图片data', mock: "@imageData" },
{ name: '颜色', mock: '@color' },
{ name: '颜色hex', mock: '@hex' },
{ name: '颜色rgba', mock: '@rgba' },
{ name: '颜色rgb', mock: '@rgb' },
{ name: '颜色hsl', mock: '@hsl' },
{ name: '整数', mock: '@integer' },
{ name: 'email', mock: '@email' },
{ name: '大段文本', mock: '@paragraph' },
{ name: '句子', mock: '@sentence' },
{ name: '单词', mock: '@word' },
{ name: '大段中文文本', mock: '@cparagraph' },
{ name: '中文标题', mock: '@ctitle' },
{ name: '标题', mock: '@title' },
{ name: '姓名', mock: '@name' },
{ name: '中文姓名', mock: '@cname' },
{ name: '中文姓', mock: '@cfirst' },
{ name: '中文名', mock: '@clast' },
{ name: '英文姓', mock: '@first' },
{ name: '英文名', mock: '@last' },
{ name: '中文句子', mock: '@csentence' },
{ name: '中文词组', mock: '@cword' },
{ name: '地址', mock: '@region' },
{ name: '省份', mock: '@province' },
{ name: '城市', mock: '@city' },
{ name: '地区', mock: '@county' },
{ name: '转换为大写', mock: '@upper' },
{ name: '转换为小写', mock: '@lower' },
{ name: '挑选(枚举)', mock: '@pick' },
{ name: '打乱数组', mock: '@shuffle' },
{ name: '协议', mock: '@protocol' }
],
IP_REGEXP: /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){3}$/,
docHref: {
adv_mock_case: 'https://hellosean1025.github.io/yapi/documents/mock.html',
adv_mock_script: 'https://hellosean1025.github.io/yapi/documents/adv_mock.html'
}
};

View File

@ -0,0 +1,215 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button, Form, Input, Icon, Tooltip, Select, message, Row, Col, Radio } from 'antd';
import { addProject } from '../../reducer/modules/project.js';
import { fetchGroupList } from '../../reducer/modules/group.js';
import { autobind } from 'core-decorators';
import { setBreadcrumb } from '../../reducer/modules/user';
const { TextArea } = Input;
const FormItem = Form.Item;
const Option = Select.Option;
const RadioGroup = Radio.Group;
import { pickRandomProperty, handlePath, nameLengthLimit } from '../../common';
import constants from '../../constants/variable.js';
import { withRouter } from 'react-router';
import './Addproject.scss';
const formItemLayout = {
labelCol: {
lg: { span: 3 },
xs: { span: 24 },
sm: { span: 6 }
},
wrapperCol: {
lg: { span: 21 },
xs: { span: 24 },
sm: { span: 14 }
},
className: 'form-item'
};
@connect(
state => {
return {
groupList: state.group.groupList,
currGroup: state.group.currGroup
};
},
{
fetchGroupList,
addProject,
setBreadcrumb
}
)
@withRouter
class ProjectList extends Component {
constructor(props) {
super(props);
this.state = {
groupList: [],
currGroupId: null
};
}
static propTypes = {
groupList: PropTypes.array,
form: PropTypes.object,
currGroup: PropTypes.object,
addProject: PropTypes.func,
history: PropTypes.object,
setBreadcrumb: PropTypes.func,
fetchGroupList: PropTypes.func
};
handlePath = e => {
let val = e.target.value;
this.props.form.setFieldsValue({
basepath: handlePath(val)
});
};
// 确认添加项目
@autobind
handleOk(e) {
const { form, addProject } = this.props;
e.preventDefault();
form.validateFields((err, values) => {
if (!err) {
values.group_id = values.group;
values.icon = constants.PROJECT_ICON[0];
values.color = pickRandomProperty(constants.PROJECT_COLOR);
addProject(values).then(res => {
if (res.payload.data.errcode == 0) {
form.resetFields();
message.success('创建成功! ');
this.props.history.push('/project/' + res.payload.data.data._id + '/interface/api');
}
});
}
});
}
async componentWillMount() {
this.props.setBreadcrumb([{ name: '新建项目' }]);
if (!this.props.currGroup._id) {
await this.props.fetchGroupList();
}
if (this.props.groupList.length === 0) {
return null;
}
this.setState({
currGroupId: this.props.currGroup._id ? this.props.currGroup._id : this.props.groupList[0]._id
});
this.setState({ groupList: this.props.groupList });
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<div className="g-row">
<div className="g-row m-container">
<Form>
<FormItem {...formItemLayout} label="项目名称">
{getFieldDecorator('name', {
rules: nameLengthLimit('项目')
})(<Input />)}
</FormItem>
<FormItem {...formItemLayout} label="所属分组">
{getFieldDecorator('group', {
initialValue: this.state.currGroupId + '',
rules: [
{
required: true,
message: '请选择项目所属的分组!'
}
]
})(
<Select>
{this.state.groupList.map((item, index) => (
<Option
disabled={
!(item.role === 'dev' || item.role === 'owner' || item.role === 'admin')
}
value={item._id.toString()}
key={index}
>
{item.group_name}
</Option>
))}
</Select>
)}
</FormItem>
<hr className="breakline" />
<FormItem
{...formItemLayout}
label={
<span>
基本路径&nbsp;
<Tooltip title="接口基本路径,为空是根路径">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
{getFieldDecorator('basepath', {
rules: [
{
required: false,
message: '请输入项目基本路径'
}
]
})(<Input onBlur={this.handlePath} />)}
</FormItem>
<FormItem {...formItemLayout} label="描述">
{getFieldDecorator('desc', {
rules: [
{
required: false,
message: '描述不超过144字!',
max: 144
}
]
})(<TextArea rows={4} />)}
</FormItem>
<FormItem {...formItemLayout} label="权限">
{getFieldDecorator('project_type', {
rules: [
{
required: true
}
],
initialValue: 'private'
})(
<RadioGroup>
<Radio value="private" className="radio">
<Icon type="lock" />私有<br />
<span className="radio-desc">只有组长和项目开发者可以索引并查看项目信息</span>
</Radio>
<br />
{/* <Radio value="public" className="radio">
<Icon type="unlock" />公开<br />
<span className="radio-desc">任何人都可以索引并查看项目信息</span>
</Radio> */}
</RadioGroup>
)}
</FormItem>
</Form>
<Row>
<Col sm={{ offset: 6 }} lg={{ offset: 3 }}>
<Button className="m-btn" icon="plus" type="primary" onClick={this.handleOk}>
创建项目
</Button>
</Col>
</Row>
</div>
</div>
);
}
}
export default Form.create()(ProjectList);

View File

@ -0,0 +1,31 @@
@import '../../styles/common.scss';
.m-container {
margin: .24rem auto !important;
padding: .24rem !important;
background-color: #fff;
}
.form-item {
margin-bottom: .16rem;
}
.breakline {
margin-top: .18rem;
margin-bottom: .18rem;
border: 0;
border-top: 1px solid #eeeeee;
}
.radio {
font-weight: 600;
}
.radio-desc {
margin-left: .22rem;
position: relative;
font-weight: normal;
top: -.08rem;
color: #919191;
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
module.exports = createDevTools(
<DockMonitor
toggleVisibilityKey="ctrl-h"
changePositionKey="ctrl-q"
defaultIsVisible={false}
>
<LogMonitor />
</DockMonitor>
);

88
vendors/client/containers/Follows/Follows.js vendored Executable file
View File

@ -0,0 +1,88 @@
import React, { PureComponent as Component } from 'react';
import './Follows.scss';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Row, Col } from 'antd';
import { getFollowList } from '../../reducer/modules/follow';
import { setBreadcrumb } from '../../reducer/modules/user';
import ProjectCard from '../../components/ProjectCard/ProjectCard.js';
import ErrMsg from '../../components/ErrMsg/ErrMsg.js';
@connect(
state => {
return {
data: state.follow.data,
uid: state.user.uid
};
},
{
getFollowList,
setBreadcrumb
}
)
class Follows extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
static propTypes = {
getFollowList: PropTypes.func,
setBreadcrumb: PropTypes.func,
uid: PropTypes.number
};
receiveRes = () => {
this.props.getFollowList(this.props.uid).then(res => {
if (res.payload.data.errcode === 0) {
this.setState({
data: res.payload.data.data.list
});
}
});
};
async componentWillMount() {
this.props.setBreadcrumb([{ name: '我的关注' }]);
this.props.getFollowList(this.props.uid).then(res => {
if (res.payload.data.errcode === 0) {
this.setState({
data: res.payload.data.data.list
});
}
});
}
render() {
let data = this.state.data;
data = data.sort((a, b) => {
return b.up_time - a.up_time;
});
return (
<div>
<div className="g-row" style={{ paddingLeft: '32px', paddingRight: '32px' }}>
<Row gutter={16} className="follow-box pannel-without-tab">
{data.length ? (
data.map((item, index) => {
return (
<Col xs={6} md={4} xl={3} key={index}>
<ProjectCard
projectData={item}
inFollowPage={true}
callbackResult={this.receiveRes}
/>
</Col>
);
})
) : (
<ErrMsg type="noFollow" />
)}
</Row>
</div>
</div>
);
}
}
export default Follows;

View File

@ -0,0 +1,9 @@
@import '../../styles/common.scss';
@import '../../styles/mixin.scss';
.follow-box{
padding: 24px;
background-color: #fff;
margin-top: .24rem;
}

123
vendors/client/containers/Group/Group.js vendored Executable file
View File

@ -0,0 +1,123 @@
import React, { PureComponent as Component } from 'react';
import GroupList from './GroupList/GroupList.js';
import ProjectList from './ProjectList/ProjectList.js';
import MemberList from './MemberList/MemberList.js';
import GroupLog from './GroupLog/GroupLog.js';
import GroupSetting from './GroupSetting/GroupSetting.js';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Route, Switch, Redirect } from 'react-router-dom';
import { Tabs, Layout, Spin } from 'antd';
const { Content, Sider } = Layout;
const TabPane = Tabs.TabPane;
import { fetchNewsData } from '../../reducer/modules/news.js';
import {
setCurrGroup
} from '../../reducer/modules/group';
import './Group.scss';
import axios from 'axios'
@connect(
state => {
return {
curGroupId: state.group.currGroup._id,
curUserRole: state.user.role,
curUserRoleInGroup: state.group.currGroup.role || state.group.role,
currGroup: state.group.currGroup
};
},
{
fetchNewsData: fetchNewsData,
setCurrGroup
}
)
export default class Group extends Component {
constructor(props) {
super(props);
this.state = {
groupId: -1
}
}
async componentDidMount(){
let r = await axios.get('/api/group/get_mygroup')
try{
let group = r.data.data;
this.setState({
groupId: group._id
})
this.props.setCurrGroup(group)
}catch(e){
console.error(e)
}
}
static propTypes = {
fetchNewsData: PropTypes.func,
curGroupId: PropTypes.number,
curUserRole: PropTypes.string,
currGroup: PropTypes.object,
curUserRoleInGroup: PropTypes.string,
setCurrGroup: PropTypes.func
};
// onTabClick=(key)=> {
// // if (key == 3) {
// // this.props.fetchNewsData(this.props.curGroupId, "group", 1, 10)
// // }
// }
render() {
if(this.state.groupId === -1)return <Spin />
const GroupContent = (
<Layout style={{ minHeight: 'calc(100vh - 100px)', marginLeft: '24px', marginTop: '24px' }}>
<Sider style={{ height: '100%' }} width={300}>
<div className="logo" />
<GroupList />
</Sider>
<Layout>
<Content
style={{
height: '100%',
margin: '0 24px 0 16px',
overflow: 'initial',
backgroundColor: '#fff'
}}
>
<Tabs type="card" className="m-tab tabs-large" style={{ height: '100%' }}>
<TabPane tab="项目列表" key="1">
<ProjectList />
</TabPane>
{this.props.currGroup.type === 'public' ? (
<TabPane tab="成员列表" key="2">
<MemberList />
</TabPane>
) : null}
{['admin', 'owner', 'guest', 'dev'].indexOf(this.props.curUserRoleInGroup) > -1 ||
this.props.curUserRole === 'admin' ? (
<TabPane tab="分组动态" key="3">
<GroupLog />
</TabPane>
) : (
''
)}
{(this.props.curUserRole === 'admin' || this.props.curUserRoleInGroup === 'owner') &&
this.props.currGroup.type !== 'private' ? (
<TabPane tab="分组设置" key="4">
<GroupSetting />
</TabPane>
) : null}
</Tabs>
</Content>
</Layout>
</Layout>
);
return (
<div className="projectGround">
<Switch>
<Redirect exact from="/group" to={"/group/" + this.state.groupId} />
<Route path="/group/:groupId" render={() => GroupContent} />
</Switch>
</div>
);
}
}

10
vendors/client/containers/Group/Group.scss vendored Executable file
View File

@ -0,0 +1,10 @@
@import '../../styles/mixin.scss';
.g-doc {
@include row-width-limit;
margin: 0 auto .24rem;
}
.news-box .news-timeline .ant-timeline-item .ant-timeline-item-content{
min-width: 300px !important;
width: 75% !important;
}

View File

@ -0,0 +1,319 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Icon, Modal, Input, message,Spin, Row, Menu, Col, Popover, Tooltip } from 'antd';
import { autobind } from 'core-decorators';
import axios from 'axios';
import { withRouter } from 'react-router-dom';
const { TextArea } = Input;
const Search = Input.Search;
import UsernameAutoComplete from '../../../components/UsernameAutoComplete/UsernameAutoComplete.js';
import GuideBtns from '../../../components/GuideBtns/GuideBtns.js';
import { fetchNewsData } from '../../../reducer/modules/news.js';
import {
fetchGroupList,
setCurrGroup,
setGroupList,
fetchGroupMsg
} from '../../../reducer/modules/group.js';
import _ from 'underscore';
import './GroupList.scss';
const tip = (
<div className="title-container">
<h3 className="title">欢迎使用 YApi ~</h3>
<p>
这里的 <b>个人空间</b>{' '}
是你自己才能看到的分组你拥有这个分组的全部权限可以在这个分组里探索 YApi 的功能
</p>
</div>
);
@connect(
state => ({
groupList: state.group.groupList,
currGroup: state.group.currGroup,
curUserRole: state.user.role,
curUserRoleInGroup: state.group.currGroup.role || state.group.role,
studyTip: state.user.studyTip,
study: state.user.study
}),
{
fetchGroupList,
setCurrGroup,
setGroupList,
fetchNewsData,
fetchGroupMsg
}
)
@withRouter
export default class GroupList extends Component {
static propTypes = {
groupList: PropTypes.array,
currGroup: PropTypes.object,
fetchGroupList: PropTypes.func,
setCurrGroup: PropTypes.func,
setGroupList: PropTypes.func,
match: PropTypes.object,
history: PropTypes.object,
curUserRole: PropTypes.string,
curUserRoleInGroup: PropTypes.string,
studyTip: PropTypes.number,
study: PropTypes.bool,
fetchNewsData: PropTypes.func,
fetchGroupMsg: PropTypes.func
};
state = {
addGroupModalVisible: false,
newGroupName: '',
newGroupDesc: '',
currGroupName: '',
currGroupDesc: '',
groupList: [],
owner_uids: []
};
constructor(props) {
super(props);
}
async componentWillMount() {
const groupId = !isNaN(this.props.match.params.groupId)
? parseInt(this.props.match.params.groupId)
: 0;
await this.props.fetchGroupList();
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) {
currGroup = this.props.groupList[0] || { group_name: '', group_desc: '' };
this.props.history.replace(`${currGroup._id}`);
}
this.setState({ groupList: this.props.groupList });
this.props.setCurrGroup(currGroup);
}
@autobind
showModal() {
this.setState({
addGroupModalVisible: true
});
}
@autobind
hideModal() {
this.setState({
newGroupName: '',
group_name: '',
owner_uids: [],
addGroupModalVisible: false
});
}
@autobind
async addGroup() {
const { newGroupName: group_name, newGroupDesc: group_desc, owner_uids } = this.state;
const res = await axios.post('/api/group/add', { group_name, group_desc, owner_uids });
if (!res.data.errcode) {
this.setState({
newGroupName: '',
group_name: '',
owner_uids: [],
addGroupModalVisible: false
});
await this.props.fetchGroupList();
this.setState({ groupList: this.props.groupList });
this.props.fetchGroupMsg(this.props.currGroup._id);
this.props.fetchNewsData(this.props.currGroup._id, 'group', 1, 10);
} else {
message.error(res.data.errmsg);
}
}
@autobind
async editGroup() {
const { currGroupName: group_name, currGroupDesc: group_desc } = this.state;
const id = this.props.currGroup._id;
const res = await axios.post('/api/group/up', { group_name, group_desc, id });
if (res.data.errcode) {
message.error(res.data.errmsg);
} else {
await this.props.fetchGroupList();
this.setState({ groupList: this.props.groupList });
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);
}
}
@autobind
inputNewGroupName(e) {
this.setState({ newGroupName: e.target.value });
}
@autobind
inputNewGroupDesc(e) {
this.setState({ newGroupDesc: e.target.value });
}
@autobind
selectGroup(e) {
const groupId = e.key;
//const currGroup = this.props.groupList.find((group) => { return +group._id === +groupId });
const currGroup = _.find(this.props.groupList, group => {
return +group._id === +groupId;
});
this.props.setCurrGroup(currGroup);
this.props.history.replace(`${currGroup._id}`);
this.props.fetchNewsData(groupId, 'group', 1, 10);
}
@autobind
onUserSelect(uids) {
this.setState({
owner_uids: uids
});
}
@autobind
searchGroup(e, value) {
const v = value || e.target.value;
const { groupList } = this.props;
if (v === '') {
this.setState({ groupList });
} else {
this.setState({
groupList: groupList.filter(group => new RegExp(v, 'i').test(group.group_name))
});
}
}
componentWillReceiveProps(nextProps) {
// GroupSetting 组件设置的分组信息通过redux同步到左侧分组菜单中
if (this.props.groupList !== nextProps.groupList) {
this.setState({
groupList: nextProps.groupList
});
}
}
render() {
const { currGroup } = this.props;
return (
<div className="m-group">
{!this.props.study ? <div className="study-mask" /> : null}
<div className="group-bar">
<div className="curr-group">
<div className="curr-group-name">
<span className="name">{currGroup.group_name}</span>
<Tooltip title="添加分组">
<a className="editSet">
<Icon className="btn" type="folder-add" onClick={this.showModal} />
</a>
</Tooltip>
</div>
<div className="curr-group-desc">简介: {currGroup.group_desc}</div>
</div>
<div className="group-operate">
<div className="search">
<Search
placeholder="搜索分类"
onChange={this.searchGroup}
onSearch={v => this.searchGroup(null, v)}
/>
</div>
</div>
{this.state.groupList.length === 0 && <Spin style={{
marginTop: 20,
display: 'flex',
justifyContent: 'center'
}} />}
<Menu
className="group-list"
mode="inline"
onClick={this.selectGroup}
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 }}
>
<Icon type="user" />
<Popover
overlayClassName="popover-index"
content={<GuideBtns />}
title={tip}
placement="right"
visible={this.props.studyTip === 0 && !this.props.study}
>
{group.group_name}
</Popover>
</Menu.Item>
);
} else {
return (
<Menu.Item key={`${group._id}`} className="group-item">
<Icon type="folder-open" />
{group.group_name}
</Menu.Item>
);
}
})}
</Menu>
</div>
{this.state.addGroupModalVisible ? (
<Modal
title="添加分组"
visible={this.state.addGroupModalVisible}
onOk={this.addGroup}
onCancel={this.hideModal}
className="add-group-modal"
>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label">分组名</div>
</Col>
<Col span="15">
<Input placeholder="请输入分组名称" onChange={this.inputNewGroupName} />
</Col>
</Row>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label">简介</div>
</Col>
<Col span="15">
<TextArea rows={3} placeholder="请输入分组描述" onChange={this.inputNewGroupDesc} />
</Col>
</Row>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label">组长</div>
</Col>
<Col span="15">
<UsernameAutoComplete callbackState={this.onUserSelect} />
</Col>
</Row>
</Modal>
) : (
''
)}
</div>
);
}
}

View File

@ -0,0 +1,147 @@
@import '../../../styles/mixin.scss';
.group-bar {
min-height: 5rem;
.curr-group {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
SimSun, sans-serif;
background-color: $color-bg-dark;
color: $color-white;
padding: .24rem .24rem 0;
.curr-group-name {
color: $color-white;
font-size: .22rem;
display: flex;
justify-content: space-between;
align-items: center;
.text {
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 140px;
font-weight: 200;
vertical-align: bottom;
}
.name{
display: inline-block;
// width: 117px;
margin-right: 20px;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
.editSet{
color: rgba(255, 255, 255, 0.85);
&:hover{
color: #2395f1;
}
}
.ant-dropdown-link{
float: right;
display: block;
color: rgba(255, 255, 255, 0.85);
&:hover{
color: #2395f1;
}
}
.operate {
font-size: 0;
width: 150px;
display: inline-block;
i{
margin-left: 4px;
}
::-webkit-scrollbar {
width: 0px;
}
}
}
.curr-group-desc {
color: $color-white-secondary;
font-size: 13px;
max-height: 54px;
margin-top: .16rem;
text-overflow:ellipsis;
overflow:hidden;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
display: -webkit-box;
}
.delete-group, .edit-group {
font-size: 14px;
margin-left: .08rem;
cursor: pointer;
border: 1px solid $color-white;
padding: 6px 12px;
border-radius: 4px;
transition: all .2s;
}
.delete-group:hover, .edit-group:hover {
background-color: $color-blue;
border: 1px solid $color-blue;
}
}
.group-operate {
padding: 16px 24px;
display: flex;
justify-content: space-around;
background-color: $color-bg-dark;
.search {
flex-grow: 1;
}
.ant-input {
color: $color-white;
background-color: $color-bg-dark;
}
.ant-input::-webkit-input-placeholder { /* Chrome/Opera/Safari */
color: $color-white-secondary;
}
.ant-input-suffix {
color: $color-white;
}
}
.group-list {
overflow-x: hidden;
border-bottom: 1px solid #e9e9e9;
padding-bottom: 24px;
border: none;
.group-item {
// height: 48px;
// line-height: 48px;
// padding: 0 24px;
font-size: 14px;
}
.group-item:hover {
// background: #34495E;
// color: $color-white;
}
.group-item.selected {
// background: #34495E;
}
.group-name {
float: left;
}
.group-edit {
float: right;
font-size: 18px;
}
}
}
.add-group-modal {
.modal-input {
margin: 24px;
}
.label {
text-align: right;
line-height: 28px;
}
}
.user-menu{
a{
&:hover{
color: #ccc;
}
}
}

View File

@ -0,0 +1,32 @@
import React, { PureComponent as Component } from 'react';
import TimeTree from '../../../components/TimeLine/TimeLine';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
// import { Button } from 'antd'
@connect(state => {
return {
uid: state.user.uid + '',
curGroupId: state.group.currGroup._id
};
})
class GroupLog extends Component {
constructor(props) {
super(props);
}
static propTypes = {
uid: PropTypes.string,
match: PropTypes.object,
curGroupId: PropTypes.number
};
render() {
return (
<div className="g-row">
<section className="news-box m-panel">
<TimeTree type={'group'} typeid={this.props.curGroupId} />
</section>
</div>
);
}
}
export default GroupLog;

View File

@ -0,0 +1,306 @@
import React, { PureComponent as Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Input, Button, message, Icon, Card, Alert, Modal, Switch, Row, Col, Tooltip } from 'antd';
import { fetchNewsData } from '../../../reducer/modules/news.js';
import {
changeGroupMsg,
fetchGroupList,
setCurrGroup,
fetchGroupMsg,
updateGroupList,
deleteGroup
} from '../../../reducer/modules/group.js';
const { TextArea } = Input;
import { trim } from '../../../common.js';
import _ from 'underscore';
import './GroupSetting.scss';
const confirm = Modal.confirm;
@connect(
state => {
return {
groupList: state.group.groupList,
currGroup: state.group.currGroup,
curUserRole: state.user.role
};
},
{
changeGroupMsg,
fetchGroupList,
setCurrGroup,
fetchGroupMsg,
fetchNewsData,
updateGroupList,
deleteGroup
}
)
class GroupSetting extends Component {
constructor(props) {
super(props);
this.state = {
currGroupDesc: '',
currGroupName: '',
showDangerOptions: false,
custom_field1_name: '',
custom_field1_enable: false,
custom_field1_rule: false
};
}
static propTypes = {
currGroup: PropTypes.object,
curUserRole: PropTypes.string,
changeGroupMsg: PropTypes.func,
fetchGroupList: PropTypes.func,
setCurrGroup: PropTypes.func,
fetchGroupMsg: PropTypes.func,
fetchNewsData: PropTypes.func,
updateGroupList: PropTypes.func,
deleteGroup: PropTypes.func,
groupList: PropTypes.array
};
initState(props) {
this.setState({
currGroupName: props.currGroup.group_name,
currGroupDesc: props.currGroup.group_desc,
custom_field1_name: props.currGroup.custom_field1.name,
custom_field1_enable: props.currGroup.custom_field1.enable
});
}
// 修改分组名称
changeName = e => {
this.setState({
currGroupName: e.target.value
});
};
// 修改分组描述
changeDesc = e => {
this.setState({
currGroupDesc: e.target.value
});
};
// 修改自定义字段名称
changeCustomName = e => {
let custom_field1_rule = this.state.custom_field1_enable ? !e.target.value : false;
this.setState({
custom_field1_name: e.target.value,
custom_field1_rule
});
};
// 修改开启状态
changeCustomEnable = e => {
let custom_field1_rule = e ? !this.state.custom_field1_name : false;
this.setState({
custom_field1_enable: e,
custom_field1_rule
});
};
componentWillMount() {
// console.log('custom_field1',this.props.currGroup.custom_field1)
this.initState(this.props);
}
// 点击“查看危险操作”按钮
toggleDangerOptions = () => {
// console.log(this.state.showDangerOptions);
this.setState({
showDangerOptions: !this.state.showDangerOptions
});
};
// 编辑分组信息
editGroup = async () => {
const id = this.props.currGroup._id;
if (this.state.custom_field1_rule) {
return;
}
const res = await this.props.changeGroupMsg({
group_name: this.state.currGroupName,
group_desc: this.state.currGroupDesc,
custom_field1: {
name: this.state.custom_field1_name,
enable: this.state.custom_field1_enable
},
id: this.props.currGroup._id
});
if (!res.payload.data.errcode) {
message.success('修改成功!');
await this.props.fetchGroupList(this.props.groupList);
this.props.updateGroupList(this.props.groupList);
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);
}
};
// 删除分组
deleteGroup = async () => {
const that = this;
const { currGroup } = that.props;
const res = await this.props.deleteGroup({ id: currGroup._id });
if (!res.payload.data.errcode) {
message.success('删除成功');
await that.props.fetchGroupList();
const currGroup = that.props.groupList[0] || { group_name: '', group_desc: '' };
that.setState({ groupList: that.props.groupList });
that.props.setCurrGroup(currGroup);
}
};
// 删除分组的二次确认
showConfirm = () => {
const that = this;
confirm({
title: '确认删除 ' + that.props.currGroup.group_name + ' 分组吗?',
content: (
<div style={{ marginTop: '10px', fontSize: '13px', lineHeight: '25px' }}>
<Alert
message="警告:此操作非常危险,会删除该分组下面所有项目和接口,并且无法恢复!"
type="warning"
/>
<div style={{ marginTop: '16px' }}>
<p>
<b>请输入分组名称确认此操作:</b>
</p>
<Input id="group_name" />
</div>
</div>
),
onOk() {
const groupName = trim(document.getElementById('group_name').value);
if (that.props.currGroup.group_name !== groupName) {
message.error('分组名称有误');
return new Promise((resolve, reject) => {
reject('error');
});
} else {
that.deleteGroup();
}
},
iconType: 'delete',
onCancel() {}
});
};
componentWillReceiveProps(nextProps) {
// 切换分组时,更新分组信息并关闭删除分组操作
if (this.props.currGroup._id !== nextProps.currGroup._id) {
this.initState(nextProps);
this.setState({
showDangerOptions: false
});
}
}
render() {
return (
<div className="m-panel card-panel card-panel-s panel-group">
<Row type="flex" justify="space-around" className="row" align="middle">
<Col span={4} className="label">
分组名
</Col>
<Col span={20}>
<Input
size="large"
placeholder="请输入分组名称"
value={this.state.currGroupName}
onChange={this.changeName}
/>
</Col>
</Row>
<Row type="flex" justify="space-around" className="row" align="middle">
<Col span={4} className="label">
简介
</Col>
<Col span={20}>
<TextArea
size="large"
rows={3}
placeholder="请输入分组描述"
value={this.state.currGroupDesc}
onChange={this.changeDesc}
/>
</Col>
</Row>
<Row type="flex" justify="space-around" className="row" align="middle">
<Col span={4} className="label">
接口自定义字段&nbsp;
<Tooltip title={'可以在接口中添加 额外字段 数据'}>
<Icon type="question-circle-o" style={{ width: '10px' }} />
</Tooltip>
</Col>
<Col span={12} style={{ position: 'relative' }}>
<Input
placeholder="请输入自定义字段名称"
style={{ borderColor: this.state.custom_field1_rule ? '#f5222d' : '' }}
value={this.state.custom_field1_name}
onChange={this.changeCustomName}
/>
<div
className="custom-field-rule"
style={{ display: this.state.custom_field1_rule ? 'block' : 'none' }}
>
自定义字段名称不能为空
</div>
</Col>
<Col span={2} className="label">
开启
</Col>
<Col span={6}>
<Switch
checked={this.state.custom_field1_enable}
checkedChildren="开"
unCheckedChildren="关"
onChange={this.changeCustomEnable}
/>
</Col>
</Row>
<Row type="flex" justify="center" className="row save">
<Col span={4} className="save-button">
<Button className="m-btn btn-save" icon="save" type="primary" onClick={this.editGroup}>
</Button>
</Col>
</Row>
{/* 只有超级管理员能删除分组 */}
{this.props.curUserRole === 'admin' ? (
<Row type="flex" justify="center" className="danger-container">
<Col span={24} className="title">
<h2 className="content">
<Icon type="exclamation-circle-o" /> 危险操作
</h2>
<Button onClick={this.toggleDangerOptions}>
<Icon type={this.state.showDangerOptions ? 'up' : 'down'} />
</Button>
</Col>
{this.state.showDangerOptions ? (
<Card hoverable={true} className="card-danger" style={{ width: '100%' }}>
<div className="card-danger-content">
<h3>删除分组</h3>
<p>分组一旦删除将无法恢复数据请慎重操作</p>
<p>只有超级管理员有权限删除分组</p>
</div>
<Button type="danger" ghost className="card-danger-btn" onClick={this.showConfirm}>
删除
</Button>
</Card>
) : null}
</Row>
) : null}
</div>
);
}
}
export default GroupSetting;

View File

@ -0,0 +1,29 @@
.panel-group {
.row {
// display: flex;
// align-items: center;
margin-bottom: .24rem;
}
.save {
margin-top: .48rem
}
.left {
flex: 100px 0 1;
text-align: right;
}
.right {
flex: 830px 0 1;
}
.label {
text-align: right;
}
.save-button{
text-align: center;
}
.custom-field-rule{
padding-top: 4px;
position: absolute;
color: #f5222d ;
}
}

View File

@ -0,0 +1,326 @@
import React, { PureComponent as 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 { Link } from 'react-router-dom';
import './MemberList.scss';
import { autobind } from 'core-decorators';
import {
fetchGroupMemberList,
fetchGroupMsg,
addMember,
delMember,
changeMemberRole
} from '../../../reducer/modules/group.js';
import ErrMsg from '../../../components/ErrMsg/ErrMsg.js';
import UsernameAutoComplete from '../../../components/UsernameAutoComplete/UsernameAutoComplete.js';
const Option = Select.Option;
function arrayAddKey(arr) {
return arr.map((item, index) => {
return {
...item,
key: index
};
});
}
@connect(
state => {
return {
currGroup: state.group.currGroup,
uid: state.user.uid,
role: state.group.role
};
},
{
fetchGroupMemberList,
fetchGroupMsg,
addMember,
delMember,
changeMemberRole
}
)
class MemberList extends Component {
constructor(props) {
super(props);
this.state = {
userInfo: [],
role: '',
visible: false,
dataSource: [],
inputUids: [],
inputRole: 'dev'
};
}
static propTypes = {
currGroup: PropTypes.object,
uid: PropTypes.number,
fetchGroupMemberList: PropTypes.func,
fetchGroupMsg: PropTypes.func,
addMember: PropTypes.func,
delMember: PropTypes.func,
changeMemberRole: PropTypes.func,
role: PropTypes.string
};
showAddMemberModal = () => {
this.setState({
visible: true
});
};
// 重新获取列表
reFetchList = () => {
this.props.fetchGroupMemberList(this.props.currGroup._id).then(res => {
this.setState({
userInfo: arrayAddKey(res.payload.data.data),
visible: false
});
});
};
// 增 - 添加成员
handleOk = () => {
this.props
.addMember({
id: this.props.currGroup._id,
member_uids: this.state.inputUids,
role: this.state.inputRole
})
.then(res => {
if (!res.payload.data.errcode) {
const { add_members, exist_members } = res.payload.data.data;
const addLength = add_members.length;
const existLength = exist_members.length;
this.setState({
inputRole: 'dev',
inputUids: []
});
message.success(`添加成功! 已成功添加 ${addLength} 人,其中 ${existLength} 人已存在`);
this.reFetchList(); // 添加成功后重新获取分组成员列表
}
});
};
// 添加成员时 选择新增成员权限
changeNewMemberRole = value => {
this.setState({
inputRole: value
});
};
// 删 - 删除分组成员
deleteConfirm = member_uid => {
return () => {
const id = this.props.currGroup._id;
this.props.delMember({ id, member_uid }).then(res => {
if (!res.payload.data.errcode) {
message.success(res.payload.data.errmsg);
this.reFetchList(); // 添加成功后重新获取分组成员列表
}
});
};
};
// 改 - 修改成员权限
changeUserRole = e => {
const id = this.props.currGroup._id;
const role = e.split('-')[0];
const member_uid = e.split('-')[1];
this.props.changeMemberRole({ id, member_uid, role }).then(res => {
if (!res.payload.data.errcode) {
message.success(res.payload.data.errmsg);
this.reFetchList(); // 添加成功后重新获取分组成员列表
}
});
};
// 关闭模态框
handleCancel = () => {
this.setState({
visible: false
});
};
componentWillReceiveProps(nextProps) {
if (this._groupId !== this._groupId) {
return null;
}
if (this.props.currGroup._id !== nextProps.currGroup._id) {
this.props.fetchGroupMemberList(nextProps.currGroup._id).then(res => {
this.setState({
userInfo: arrayAddKey(res.payload.data.data)
});
});
this.props.fetchGroupMsg(nextProps.currGroup._id).then(res => {
this.setState({
role: res.payload.data.data.role
});
});
}
}
componentDidMount() {
const currGroupId = (this._groupId = this.props.currGroup._id);
this.props.fetchGroupMsg(currGroupId).then(res => {
this.setState({
role: res.payload.data.data.role
});
});
this.props.fetchGroupMemberList(currGroupId).then(res => {
this.setState({
userInfo: arrayAddKey(res.payload.data.data)
});
});
}
@autobind
onUserSelect(uids) {
this.setState({
inputUids: uids
});
}
render() {
const columns = [
{
title:
this.props.currGroup.group_name + ' 分组成员 (' + this.state.userInfo.length + ') 人',
dataIndex: 'username',
key: 'username',
render: (text, record) => {
return (
<div className="m-user">
<Link to={`/user/profile/${record.uid}`}>
<img
src={
location.protocol + '//' + location.host + '/api/user/avatar?uid=' + record.uid
}
className="m-user-img"
/>
</Link>
<Link to={`/user/profile/${record.uid}`}>
<p className="m-user-name">{text}</p>
</Link>
</div>
);
}
},
{
title:
this.state.role === 'owner' || this.state.role === 'admin' ? (
<div className="btn-container">
<Button className="btn" type="primary" onClick={this.showAddMemberModal}>
添加成员
</Button>
</div>
) : (
''
),
key: 'action',
className: 'member-opration',
render: (text, record) => {
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>
<Popconfirm
placement="topRight"
title="你确定要删除吗? "
onConfirm={this.deleteConfirm(record.uid)}
okText="确定"
cancelText=""
>
<Button type="danger" icon="delete" className="btn-danger" />
{/* <Icon type="delete" className="btn-danger"/> */}
</Popconfirm>
</div>
);
} else {
// 非管理员可以看到权限 但无法修改
if (record.role === 'owner') {
return '组长';
} else if (record.role === 'dev') {
return '开发者';
} else if (record.role === 'guest') {
return '访客';
} else {
return '';
}
}
}
}
];
let userinfo = this.state.userInfo;
let ownerinfo = [];
let devinfo = [];
let guestinfo = [];
for (let i = 0; i < userinfo.length; i++) {
if (userinfo[i].role === 'owner') {
ownerinfo.push(userinfo[i]);
}
if (userinfo[i].role === 'dev') {
devinfo.push(userinfo[i]);
}
if (userinfo[i].role === 'guest') {
guestinfo.push(userinfo[i]);
}
}
userinfo = [...ownerinfo, ...devinfo, ...guestinfo];
return (
<div className="m-panel">
{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">
<UsernameAutoComplete callbackState={this.onUserSelect} />
</Col>
</Row>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label usernameauth">权限: </div>
</Col>
<Col span="15">
<Select defaultValue="dev" className="select" onChange={this.changeNewMemberRole}>
<Option value="owner">组长</Option>
<Option value="dev">开发者</Option>
<Option value="guest">访客</Option>
</Select>
</Col>
</Row>
</Modal>
) : (
''
)}
<Table
columns={columns}
dataSource={userinfo}
pagination={false}
locale={{ emptyText: <ErrMsg type="noMemberInGroup" /> }}
/>
</div>
);
}
}
export default MemberList;

View File

@ -0,0 +1,58 @@
@import '../../../styles/mixin.scss';
.m-panel{
background-color: #fff;
padding: 24px;
min-height: 4.68rem;
margin-top: 0;
// box-shadow: $box-shadow-panel;
}
.m-tab {
overflow: inherit !important;
}
.btn-container {
text-align: right;
}
.modal-input {
display: flex;
align-items: center;
margin-bottom: .24rem;
.label {
text-align: right;
}
.select {
width: 1.2rem;
}
}
.member-opration {
text-align: right;
.select {
width: 1.2rem;
}
.btn-danger {
margin-left: .08rem;
// background-color: transparent;
border-color: transparent;
}
}
.m-user {
display: flex;
align-items: center;
.m-user-img {
width: .32rem;
height: .32rem;
border-radius: .04rem;
}
.m-user-name {
margin-left: 8px;
}
}
.usernamelabel,.usernameauth{
line-height: 36px;
}

View File

@ -0,0 +1,217 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Row, Col, Button, Tooltip } from 'antd';
import { Link } from 'react-router-dom';
import {
addProject,
fetchProjectList,
delProject,
changeUpdateModal
} from '../../../reducer/modules/project';
import ProjectCard from '../../../components/ProjectCard/ProjectCard.js';
import ErrMsg from '../../../components/ErrMsg/ErrMsg.js';
import { autobind } from 'core-decorators';
import { setBreadcrumb } from '../../../reducer/modules/user';
import './ProjectList.scss';
@connect(
state => {
return {
projectList: state.project.projectList,
userInfo: state.project.userInfo,
tableLoading: state.project.tableLoading,
currGroup: state.group.currGroup,
currPage: state.project.currPage
};
},
{
fetchProjectList,
addProject,
delProject,
changeUpdateModal,
setBreadcrumb
}
)
class ProjectList extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
protocol: 'http://',
projectData: []
};
}
static propTypes = {
form: PropTypes.object,
fetchProjectList: PropTypes.func,
addProject: PropTypes.func,
delProject: PropTypes.func,
changeUpdateModal: PropTypes.func,
projectList: PropTypes.array,
userInfo: PropTypes.object,
tableLoading: PropTypes.bool,
currGroup: PropTypes.object,
setBreadcrumb: PropTypes.func,
currPage: PropTypes.number,
studyTip: PropTypes.number,
study: PropTypes.bool
};
// 取消修改
@autobind
handleCancel() {
this.props.form.resetFields();
this.setState({
visible: false
});
}
// 修改线上域名的协议类型 (http/https)
@autobind
protocolChange(value) {
this.setState({
protocol: value
});
}
// 获取 ProjectCard 组件的关注事件回调,收到后更新数据
receiveRes = () => {
this.props.fetchProjectList(this.props.currGroup._id, this.props.currPage);
};
componentWillReceiveProps(nextProps) {
this.props.setBreadcrumb([{ name: '' + (nextProps.currGroup.group_name || '') }]);
// 切换分组
if (this.props.currGroup !== nextProps.currGroup && nextProps.currGroup._id) {
this.props.fetchProjectList(nextProps.currGroup._id, this.props.currPage);
}
// 切换项目列表
if (this.props.projectList !== nextProps.projectList) {
// console.log(nextProps.projectList);
const data = nextProps.projectList.map((item, index) => {
item.key = index;
return item;
});
this.setState({
projectData: data
});
}
}
render() {
let projectData = this.state.projectData;
let noFollow = [];
let followProject = [];
for (var i in projectData) {
if (projectData[i].follow) {
followProject.push(projectData[i]);
} else {
noFollow.push(projectData[i]);
}
}
followProject = followProject.sort((a, b) => {
return b.up_time - a.up_time;
});
noFollow = noFollow.sort((a, b) => {
return b.up_time - a.up_time;
});
projectData = [...followProject, ...noFollow];
const isShow = /(admin)|(owner)|(dev)/.test(this.props.currGroup.role);
const Follow = () => {
return followProject.length ? (
<Row>
<h3 className="owner-type">我的关注</h3>
{followProject.map((item, index) => {
return (
<Col xs={8} lg={6} xxl={4} key={index}>
<ProjectCard projectData={item} callbackResult={this.receiveRes} />
</Col>
);
})}
</Row>
) : null;
};
const NoFollow = () => {
return noFollow.length ? (
<Row style={{ borderBottom: '1px solid #eee', marginBottom: '15px' }}>
<h3 className="owner-type">我的项目</h3>
{noFollow.map((item, index) => {
return (
<Col xs={8} lg={6} xxl={4} key={index}>
<ProjectCard projectData={item} callbackResult={this.receiveRes} isShow={isShow} />
</Col>
);
})}
</Row>
) : null;
};
const OwnerSpace = () => {
return projectData.length ? (
<div>
<NoFollow />
<Follow />
</div>
) : (
<ErrMsg type="noProject" />
);
};
return (
<div style={{ paddingTop: '24px' }} className="m-panel card-panel card-panel-s project-list">
<Row className="project-list-header">
<Col span={16} style={{ textAlign: 'left' }}>
{this.props.currGroup.group_name} 分组共 ({projectData.length}) 个项目
</Col>
<Col span={8}>
{isShow ? (
<Link to="/add-project">
<Button type="primary">添加项目</Button>
</Link>
) : (
<Tooltip title="您没有权限,请联系该分组组长或管理员">
<Button type="primary" disabled>
添加项目
</Button>
</Tooltip>
)}
</Col>
</Row>
<Row>
{/* {projectData.length ? projectData.map((item, index) => {
return (
<Col xs={8} md={6} xl={4} key={index}>
<ProjectCard projectData={item} callbackResult={this.receiveRes} />
</Col>);
}) : <ErrMsg type="noProject" />} */}
{this.props.currGroup.type === 'private' ? (
<OwnerSpace />
) : projectData.length ? (
projectData.map((item, index) => {
return (
<Col xs={8} lg={6} xxl={4} key={index}>
<ProjectCard
projectData={item}
callbackResult={this.receiveRes}
isShow={isShow}
/>
</Col>
);
})
) : (
<ErrMsg type="noProject" />
)}
</Row>
</div>
);
}
}
export default ProjectList;

View File

@ -0,0 +1,62 @@
.ant-tabs-bar {
border-bottom: 1px solid transparent;
margin-bottom: 0;
}
.m-panel{
background-color: #fff;
padding: 24px;
min-height: 4.68rem;
margin-top: 0;
}
.project-list{
.project-list-header{
background: #eee;
height: 64px;
line-height: 40px;
border-radius: 4px;
text-align: right;
padding: 0 10px;
font-weight: bold;
margin-bottom: 15px;
display: flex;
align-items: center;
color: rgba(39, 56, 72, 0.85);
font-weight: 500;
}
.owner-type{
// padding: 10px;
font-size: 15px;
// background-color: #eee;
// border-radius: 4px;
// margin-bottom: 15px;
font-weight: 400;
margin-bottom: 0.16rem;
border-left: 3px solid #2395f1;
padding-left: 8px;
}
}
.ant-input-group-wrapper {
width: 100%;
}
.dynamic-delete-button {
cursor: pointer;
position: relative;
top: 4px;
font-size: 24px;
color: #999;
transition: all .3s;
}
.dynamic-delete-button:hover {
color: #777;
}
.dynamic-delete-button[disabled] {
cursor: not-allowed;
opacity: 0.5;
}

View File

@ -0,0 +1,387 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Modal, Form, Input, Icon, Tooltip, Select, message, Button, Row, Col } from 'antd';
import {
updateProject,
fetchProjectList,
delProject,
changeUpdateModal,
changeTableLoading
} from '../../../reducer/modules/project';
const { TextArea } = Input;
const FormItem = Form.Item;
const Option = Select.Option;
import './ProjectList.scss';
// layout
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 14 }
}
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 20, offset: 6 }
}
};
let uuid = 0;
@connect(
state => {
return {
projectList: state.project.projectList,
isUpdateModalShow: state.project.isUpdateModalShow,
handleUpdateIndex: state.project.handleUpdateIndex,
tableLoading: state.project.tableLoading,
currGroup: state.group.currGroup
};
},
{
fetchProjectList,
updateProject,
delProject,
changeUpdateModal,
changeTableLoading
}
)
class UpDateModal extends Component {
constructor(props) {
super(props);
this.state = {
protocol: 'http://',
envProtocolChange: 'http://'
};
}
static propTypes = {
form: PropTypes.object,
fetchProjectList: PropTypes.func,
updateProject: PropTypes.func,
delProject: PropTypes.func,
changeUpdateModal: PropTypes.func,
changeTableLoading: PropTypes.func,
projectList: PropTypes.array,
currGroup: PropTypes.object,
isUpdateModalShow: PropTypes.bool,
handleUpdateIndex: PropTypes.number
};
// 修改线上域名的协议类型 (http/https)
protocolChange = value => {
this.setState({
protocol: value
});
};
handleCancel = () => {
this.props.form.resetFields();
this.props.changeUpdateModal(false, -1);
};
// 确认修改
handleOk = e => {
e.preventDefault();
const {
form,
updateProject,
changeUpdateModal,
currGroup,
projectList,
handleUpdateIndex,
fetchProjectList,
changeTableLoading
} = this.props;
form.validateFields((err, values) => {
if (!err) {
// console.log(projectList[handleUpdateIndex]);
let assignValue = Object.assign(projectList[handleUpdateIndex], values);
values.protocol = this.state.protocol.split(':')[0];
assignValue.env = assignValue.envs.map((item, index) => {
return {
name: values['envs-name-' + index],
domain: values['envs-protocol-' + index] + values['envs-domain-' + index]
};
});
// console.log(assignValue);
changeTableLoading(true);
updateProject(assignValue)
.then(res => {
if (res.payload.data.errcode == 0) {
changeUpdateModal(false, -1);
message.success('修改成功! ');
fetchProjectList(currGroup._id).then(() => {
changeTableLoading(false);
});
} else {
changeTableLoading(false);
message.error(res.payload.data.errmsg);
}
})
.catch(() => {
changeTableLoading(false);
});
form.resetFields();
}
});
};
// 项目的修改操作 - 删除一项环境配置
remove = id => {
const { form } = this.props;
// can use data-binding to get
const envs = form.getFieldValue('envs');
// We need at least one passenger
if (envs.length === 0) {
return;
}
// can use data-binding to set
form.setFieldsValue({
envs: envs.filter(key => {
const realKey = key._id ? key._id : key;
return realKey !== id;
})
});
};
// 项目的修改操作 - 添加一项环境配置
add = () => {
uuid++;
const { form } = this.props;
// can use data-binding to get
const envs = form.getFieldValue('envs');
const nextKeys = envs.concat(uuid);
// can use data-binding to set
// important! notify form to detect changes
form.setFieldsValue({
envs: nextKeys
});
};
render() {
const { getFieldDecorator, getFieldValue } = this.props.form;
// const that = this;
const { isUpdateModalShow, projectList, handleUpdateIndex } = this.props;
let initFormValues = {};
let envMessage = [];
// 如果列表存在且用户点击修改按钮时,设置表单默认值
if (projectList.length !== 0 && handleUpdateIndex !== -1) {
// console.log(projectList[handleUpdateIndex]);
const { name, basepath, desc, env } = projectList[handleUpdateIndex];
initFormValues = { name, basepath, desc, env };
if (env.length !== 0) {
envMessage = env;
}
initFormValues.prd_host = projectList[handleUpdateIndex].prd_host;
initFormValues.prd_protocol = projectList[handleUpdateIndex].protocol + '://';
}
getFieldDecorator('envs', { initialValue: envMessage });
const envs = getFieldValue('envs');
const formItems = envs.map((k, index) => {
const secondIndex = 'next' + index; // 为保证key的唯一性
return (
<Row key={index} type="flex" justify="space-between" align={index === 0 ? 'middle' : 'top'}>
<Col span={10} offset={2}>
<FormItem label={index === 0 ? <span>环境名称</span> : ''} required={false} key={index}>
{getFieldDecorator(`envs-name-${index}`, {
validateTrigger: ['onChange', 'onBlur'],
initialValue: envMessage.length !== 0 ? k.name : '',
rules: [
{
required: false,
whitespace: true,
validator(rule, value, callback) {
if (value) {
if (value.length === 0) {
callback('请输入环境域名');
} else if (!/\S/.test(value)) {
callback('请输入环境域名');
} else if (/prd/.test(value)) {
callback('环境域名不能是"prd"');
} else {
return callback();
}
} else {
callback('请输入环境域名');
}
}
}
]
})(<Input placeholder="请输入环境名称" style={{ width: '90%', marginRight: 8 }} />)}
</FormItem>
</Col>
<Col span={10}>
<FormItem
label={index === 0 ? <span>环境域名</span> : ''}
required={false}
key={secondIndex}
>
{getFieldDecorator(`envs-domain-${index}`, {
validateTrigger: ['onChange', 'onBlur'],
initialValue: envMessage.length !== 0 && k.domain ? k.domain.split('//')[1] : '',
rules: [
{
required: false,
whitespace: true,
message: '请输入环境域名',
validator(rule, value, callback) {
if (value) {
if (value.length === 0) {
callback('请输入环境域名');
} else if (!/\S/.test(value)) {
callback('请输入环境域名');
} else {
return callback();
}
} else {
callback('请输入环境域名');
}
}
}
]
})(
<Input
placeholder="请输入环境域名"
style={{ width: '90%', marginRight: 8 }}
addonBefore={getFieldDecorator(`envs-protocol-${index}`, {
initialValue:
envMessage.length !== 0 && k.domain
? k.domain.split('//')[0] + '//'
: 'http://',
rules: [
{
required: true
}
]
})(
<Select>
<Option value="http://">{'http://'}</Option>
<Option value="https://">{'https://'}</Option>
</Select>
)}
/>
)}
</FormItem>
</Col>
<Col span={2}>
{/* 新增的项中,只有最后一项有删除按钮 */}
{(envs.length > 0 && k._id) || envs.length == index + 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => {
return this.remove(k._id ? k._id : k);
}}
/>
) : null}
</Col>
</Row>
);
});
return (
<Modal
title="修改项目"
visible={isUpdateModalShow}
onOk={this.handleOk}
onCancel={this.handleCancel}
>
<Form>
<FormItem {...formItemLayout} label="项目名称">
{getFieldDecorator('name', {
initialValue: initFormValues.name,
rules: [
{
required: true,
message: '请输入项目名称!'
}
]
})(<Input />)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
线上域名&nbsp;
<Tooltip title="将根据配置的线上域名访问mock数据">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
{getFieldDecorator('prd_host', {
initialValue: initFormValues.prd_host,
rules: [
{
required: true,
message: '请输入项目线上域名!'
}
]
})(
<Input
addonBefore={
<Select defaultValue={initFormValues.prd_protocol} onChange={this.protocolChange}>
<Option value="http://">{'http://'}</Option>
<Option value="https://">{'https://'}</Option>
</Select>
}
/>
)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
基本路径&nbsp;
<Tooltip title="基本路径为空表示根路径">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
{getFieldDecorator('basepath', {
initialValue: initFormValues.basepath,
rules: [
{
required: false,
message: '请输入项目基本路径! '
}
]
})(<Input />)}
</FormItem>
<FormItem {...formItemLayout} label="描述">
{getFieldDecorator('desc', {
initialValue: initFormValues.desc,
rules: [
{
required: false,
message: '请输入描述!'
}
]
})(<TextArea rows={4} />)}
</FormItem>
{formItems}
<FormItem {...formItemLayoutWithOutLabel}>
<Button type="dashed" onClick={this.add} style={{ width: '60%' }}>
<Icon type="plus" /> 添加环境配置
</Button>
</FormItem>
</Form>
</Modal>
);
}
}
export default Form.create()(UpDateModal);

409
vendors/client/containers/Home/Home.js vendored Executable file
View File

@ -0,0 +1,409 @@
import './Home.scss';
import React, { PureComponent as Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Row, Col, Button, Icon, Card } from 'antd';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import LogoSVG from '../../components/LogoSVG/index.js';
import { changeMenuItem } from '../../reducer/modules/menu';
const plugin = require('client/plugin.js');
const ThirdLogin = plugin.emitHook('third_login');
const HomeGuest = () => (
<div className="g-body">
<div className="m-bg">
<div className="m-bg-mask m-bg-mask0" />
<div className="m-bg-mask m-bg-mask1" />
<div className="m-bg-mask m-bg-mask2" />
<div className="m-bg-mask m-bg-mask3" />
</div>
<div className="main-one">
<div className="container">
<Row>
<Col span={24}>
<div className="home-header">
<a href="#" className="item">
YAPI
</a>
<a
target="_blank"
rel="noopener noreferrer"
href="https://hellosean1025.github.io/yapi"
className="item"
>
使用文档
</a>
</div>
</Col>
</Row>
<Row>
<Col lg={9} xs={24}>
<div className="home-des">
<div className="logo">
<LogoSVG length="72px" />
<span className="name">YAPI</span>
</div>
<div className="detail">
高效易用功能强大的API管理平台<br />
<span className="desc">旨在为开发产品测试人员提供更优雅的接口管理服务</span>
</div>
<div className="btn-group">
<Link to="/login">
<Button type="primary" className="btn-home btn-login">
登录 / 注册
</Button>
</Link>
{ThirdLogin != null ? <ThirdLogin /> : null}
</div>
</div>
</Col>
<Col lg={15} xs={0} className="col-img">
<div className="img-container">
</div>
</Col>
</Row>
</div>
</div>
<div className="feat-part section-feature">
<div className="container home-section">
<h3 className="title">为API开发者设计的管理平台</h3>
<span className="desc">
YApi让接口开发更简单高效让接口的管理更具可读性可维护性让团队协作更合理
</span>
<Row key="feat-motion-row">
<Col span={8} className="section-item" key="feat-wrapper-1">
<Icon type="appstore-o" className="img" />
<h4 className="title">项目管理</h4>
<span className="desc">提供基本的项目分组项目管理接口管理功能</span>
</Col>
<Col span={8} className="section-item" key="feat-wrapper-2">
<Icon type="api" className="img" />
<h4 className="title">接口管理</h4>
<span className="desc">
友好的接口文档基于websocket的多人协作接口编辑功能和类postman测试工具让多人协作成倍提升开发效率
</span>
</Col>
<Col span={8} className="section-item" key="feat-wrapper-3">
<Icon type="database" className="img" />
<h4 className="title">MockServer</h4>
<span className="desc">基于Mockjs使用简单功能强大</span>
</Col>
</Row>
</div>
</div>
<div className="feat-part m-mock m-skew home-section">
<div className="m-skew-bg">
<div className="m-bg-mask m-bg-mask0" />
<div className="m-bg-mask m-bg-mask1" />
<div className="m-bg-mask m-bg-mask2" />
</div>
<div className="container skew-container">
<h3 className="title">功能强大的 Mock 服务</h3>
<span className="desc">你想要的 Mock 服务都在这里</span>
<Row className="row-card">
<Col lg={12} xs={24} className="section-card">
<Card title="Mock 规则">
<p className="mock-desc">
通过学习一些简单的 Mock
模板规则即可轻松编写接口这将大大提高定义接口的效率并且无需为编写 Mock 数据烦恼:
所有的数据都可以实时随机生成
</p>
<div className="code">
<ol start="1">
<li className="item">
<span className="orderNum orderNum-first">1</span>
<span>
<span>&#123;&ensp;&ensp;</span>
</span>
</li>
<li className="item">
<span className="orderNum">2</span>
<span>
&ensp;&ensp;&ensp;&ensp;<span className="string">
&quot;errcode|200-500&quot;
</span>
<span>
:&ensp;<span className="number">200</span>,&ensp;&ensp;
</span>
</span>
</li>
<li className="item">
<span className="orderNum">3</span>
<span>
&ensp;&ensp;&ensp;&ensp;<span className="string">&quot;errmsg|4-8&quot;</span>
<span>:&ensp;</span>
<span className="string">&quot;@string&quot;</span>
<span>,&ensp;&ensp;</span>
</span>
</li>
<li className="item">
<span className="orderNum">4</span>
<span>
&ensp;&ensp;&ensp;&ensp;<span className="string">&quot;data&quot;</span>
<span>:&ensp;&#123;&ensp;&ensp;</span>
</span>
</li>
<li className="item">
<span className="orderNum">5</span>
<span>
&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;<span className="string">
&quot;boolean|1&quot;
</span>
<span>:&ensp;</span>
<span className="keyword">true</span>
<span>,&ensp;&ensp;</span>
</span>
</li>
<li className="item">
<span className="orderNum">6</span>
<span>
&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;<span className="string">
&quot;array|2&quot;
</span>
<span>
:&ensp;&#91;<span className="string">&quot;Bob&quot;</span>,&ensp;<span className="string">
&quot;Jim&quot;
</span>&#93;,&ensp;&ensp;
</span>
</span>
</li>
<li className="item">
<span className="orderNum">7</span>
<span>
&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;<span className="string">
&quot;combine&quot;
</span>
<span>:&ensp;</span>
<span className="string">&quot;@boolean&ensp;&amp;&ensp;@array&quot;</span>
<span>&ensp;&ensp;</span>
</span>
</li>
<li className="item">
<span className="orderNum">8</span>
<span>&ensp;&ensp;&ensp;&ensp;&#125;&ensp;&ensp;</span>
</li>
<li className="item">
<span className="orderNum orderNum-last">9</span>
<span>&#125;&ensp;&ensp;</span>
</li>
</ol>
</div>
</Card>
</Col>
<Col lg={12} xs={24} className="section-card mock-after">
<Card title="生成的 Mock 数据">
<p className="mock-desc">
生成的 Mock 数据可以直接用 ajax
请求使用也可以通过服务器代理使用不需要修改项目一行代码
</p>
<div className="code">
<ol start="1">
<li className="alt">
<span className="orderNum orderNum-first">1</span>
<span>
<span>&#123;&ensp;&ensp;</span>
</span>
</li>
<li className="">
<span className="orderNum">2</span>
<span>
&ensp;&ensp;<span className="string">&quot;errcode&quot;</span>
<span>:&ensp;</span>
<span className="number">304</span>
<span>,&ensp;&ensp;</span>
</span>
</li>
<li className="alt">
<span className="orderNum">3</span>
<span>
&ensp;&ensp;<span className="string">&quot;errmsg&quot;</span>
<span>:&ensp;</span>
<span className="string">&quot;JtkMIoRu)N#ie^h%Z77[F)&quot;</span>
<span>,&ensp;&ensp;</span>
</span>
</li>
<li className="">
<span className="orderNum">4</span>
<span>
&ensp;&ensp;<span className="string">&quot;data&quot;</span>
<span>:&ensp;&#123;&ensp;&ensp;</span>
</span>
</li>
<li className="alt">
<span className="orderNum">5</span>
<span>
&ensp;&ensp;&ensp;&ensp;<span className="string">&quot;boolean&quot;</span>
<span>:&ensp;</span>
<span className="keyword">true</span>
<span>,&ensp;&ensp;</span>
</span>
</li>
<li className="">
<span className="orderNum">6</span>
<span>
&ensp;&ensp;&ensp;&ensp;<span className="string">&quot;array&quot;</span>
<span>
:&ensp;
</span>&#91;<span className="string">&quot;Bob&quot;</span>,&ensp;<span className="string">
&quot;Jim&quot;
</span>,&ensp;<span className="string">&quot;Bob&quot;</span>,&ensp;<span className="string">
&quot;Jim&quot;
</span>&#93;<span>,&ensp;&ensp;</span>
</span>
</li>
<li className="alt">
<span className="orderNum">7</span>
<span>
&ensp;&ensp;&ensp;&ensp;<span className="string">&quot;combine&quot;</span>
<span>:&ensp;</span>
<span className="string">
&quot;true & Bob,&ensp;Jim,&ensp;Bob,&ensp;Jim&quot;
</span>
<span>&ensp;&ensp;</span>
</span>
</li>
<li className="">
<span className="orderNum">8</span>
<span>&ensp;&ensp;&#125;&ensp;&ensp;</span>
</li>
<li className="alt">
<span className="orderNum orderNum-last">9</span>
<span>&#125;&ensp;&ensp;</span>
</li>
</ol>
</div>
</Card>
</Col>
</Row>
</div>
</div>
<div className="home-section section-manage">
<div className="container">
<Row className="row-card" style={{ marginBottom: '.48rem' }}>
<Col lg={7} xs={10} className="section-card">
<Card>
<div className="section-block block-first">
<h4>超级管理员(* N)</h4>
<p className="item"> - 创建分组</p>
<p className="item"> - 分配组长</p>
<p className="item"> - 管理所有成员信息</p>
</div>
<div className="section-block block-second">
<h4>组长(* N)</h4>
<p className="item"> - 创建项目</p>
<p className="item"> - 管理分组或项目的信息</p>
<p className="item"> - 管理开发者与成员</p>
</div>
<div className="section-block block-third">
<h4>开发者(* N) / 成员(* N)</h4>
<p className="item"> - 不允许创建分组</p>
<p className="item"> - 不允许修改分组或项目信息</p>
</div>
</Card>
</Col>
<Col lg={17} xs={14} className="section-card manage-word">
<Icon type="team" className="icon" />
<h3 className="title">扁平化管理模式</h3>
<p className="desc">
接口管理的逻辑较为复杂操作频率高层层审批将严重拖慢生产效率因此传统的金字塔管理模式并不适用
</p>
<p className="desc">
YAPI
将扁平化管理模式的思想引入到产品的权限管理中超级管理员拥有最高的权限并将权限分配给若干组长超级管理员只需管理组长即可实际上管理YAPI各大分组与项目的是组长组长对分组或项目负责一般由BU负责人/项目负责人担任
</p>
</Col>
</Row>
</div>
</div>
</div>
);
HomeGuest.propTypes = {
introList: PropTypes.array
};
@connect(
state => ({
login: state.user.isLogin
}),
{
changeMenuItem
}
)
@withRouter
class Home extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
if (this.props.login) {
this.props.history.push('/group/261');
}
}
componentDidMount() {}
static propTypes = {
introList: PropTypes.array,
login: PropTypes.bool,
history: PropTypes.object,
changeMenuItem: PropTypes.func
};
toStart = () => {
this.props.changeMenuItem('/group');
};
render() {
return (
<div className="home-main">
<HomeGuest introList={this.props.introList} />
<div className="row-tip">
<div className="container">
<div className="tip-title">
<h3 className="title">准备好使用了吗</h3>
<p className="desc">注册账号尽请使用吧查看使用文档了解更多信息</p>
</div>
<div className="tip-btns">
<div className="btn-group">
<Link to="/login">
<Button type="primary" className="btn-home btn-login">
登录 / 注册
</Button>
</Link>
<Button className="btn-home btn-home-normal">
<a target="_blank" rel="noopener noreferrer" href="https://hellosean1025.github.io/yapi">
使用文档
</a>
</Button>
</div>
</div>
</div>
</div>
</div>
);
}
}
// Home.defaultProps={
// introList:[{
// title:"接口管理",
// des:"满足你的所有接口管理需求。不再需要为每个项目搭建独立的接口管理平台和编写离线的接口文档,其权限管理和项目日志让协作开发不再痛苦。",
// detail:[
// {title:"团队协作",des:"多成员协作,掌握项目进度",iconType:"team"},
// {title:"权限管理",des:"设置每个成员的操作权限",iconType:"usergroup-add"},
// {title:"项目日志",des:"推送项目情况,掌握更新动态",iconType:"schedule"}
// ],
// img:"./image/demo-img.jpg"
// },{
// title:"接口测试",
// des:"一键即可得到返回结果。根据用户的输入接口信息如协议、URL、接口名、请求头、请求参数、mock规则生成Mock接口这些接口会自动生成模拟数据。",
// detail:[
// {title:"编辑接口",des:"团队开发时任何人都可以在权限许可下创建、修改接口",iconType:"tags-o"},
// {title:"mock请求",des:"创建者可以自由构造需要的数据,支持复杂的生成逻辑",iconType:"fork"}
// ],
// img:"./image/demo-img.jpg"
// }
// ]
// };
export default Home;

528
vendors/client/containers/Home/Home.scss vendored Executable file
View File

@ -0,0 +1,528 @@
@import '../../styles/mixin.scss';
$color-white : #fff;
$color-blue-lighter : #f1f5ff;
$color-blue-grey-lighter : #f7fafc;
$color-grey-lighter : #F7F7F7;
$color-blue-light: #5dade2;
$color-black-lighter: #404040;
$color-text-dark: #2e2e5a;
$color-text-light: #6d7c90;
$color-bg-lightblue: #c6e2ff;
.g-body {
position: relative;
}
.home-header {
font-size: 0;
.item {
text-decoration: none;
display: inline-block;
color: #fff;
font-size: .17rem;
padding: .16rem .24rem;
-webkit-font-smoothing: antialiased;
&:hover {
color: #27cdfd;
}
}
}
// 按钮组
.btn-group {
padding: .3rem .24rem;
.btn-home {
font-size: .15rem;
font-weight: 200;
letter-spacing: 1px;
border: none;
line-height: .4rem;
height: .4rem;
padding: 0 .24rem;
margin-right: .24rem;
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
transform: translateY(0);
transition: all .2s;
&:hover {
transform: translateY(-1px);
box-shadow: 0 7px 14px rgba(50,50,93,.1), 0 3px 6px rgba(0,0,0,.08);
}
&:active {
transform: translateY(1px);
}
}
.btn-login {
background-color: #32325d;
&:hover {
background-color: #43459a;
}
&:active, &:focus {
color: #e6ebf1;
background-color: #32325d;
}
}
.btn-home-normal {
border-radius: 4px;
color: #43459a;
&:hover {
color: #7795f8;
}
&:hover, &:focus {
background-color: #f6f9fc;
}
background-color: #fff;
cursor: pointer;
}
}
.m-bg {
position: absolute;
left: 0;
top: -400px;
height: 1000px;
width: 100%;
transform: skewY(-11deg);
background-image: linear-gradient(-20deg, #21d4fd 0%, #b721ff 100%);
.m-bg-mask {
position: absolute;
height: 180px;
}
.m-bg-mask0 {
bottom: 0;
left: 0;
width: 30%;
background-image: linear-gradient(120deg, #6ab3fd 0%, #8ba3fd 102%);
}
.m-bg-mask1 {
bottom: 180px;
right: 0;
width: 36%;
background-image: linear-gradient(120deg, #28c5f5 0%, #6682fe 100%);
}
.m-bg-mask2 {
bottom: 540px;
left: 0;
width: 20%;
height: 240px;
background-image: linear-gradient(120deg, #8121ff 0%, #5e5ef7 100%);
}
.m-bg-mask3 {
bottom: 540px;
left: 20%;
width: 70%;
height: 240px;
background-image: linear-gradient(-225deg, #5f2bff 0%, #6088fe 48%, #22ccf6 100%);
}
}
.home-main {
background-color: #fff;
display: -webkit-box;
-webkit-box-orient: vertical;
.main-one{
height: 600px;
.home-des{
padding: 1rem 0 0;
color: #fff;
.title{
font-size: .6rem;
}
.detail{
font-size: .2rem;
}
.logo {
display: flex;
align-items: center;
padding: 0 .24rem;
}
.svg {
animation: spin 5s linear infinite;
}
.name {
vertical-align: middle;
font-size: .48rem;
margin-left: .24rem;
font-weight: 200;
}
.detail {
padding: .24rem;
font-size: .24rem;
font-weight: 200;
}
.desc {
font-size: .18rem;
}
}
.login-form{
color: $color-white;
}
.main-one-left{
padding-right: .15rem;
margin-top: .2rem;
}
.main-one-right{
padding-left: .5rem;
padding-top: .3rem;
}
}
.user-home{
display: flex;
align-items: center;
height: 100%;
margin: 1rem auto 0;
.user-des{
margin: 0 auto .5rem;
text-align: center;
.title{
font-size: .8rem;
margin-bottom: .2rem;
}
.des{
font-size: .25rem;
margin-bottom: .3rem;
}
.btn{
button{
font-size: .2rem;
line-height: .2rem;
height: .5rem;
padding: .15rem .5rem;
}
}
}
}
.main-part{
padding: 1.5rem 0;
height: 5.8rem;
&:nth-child(odd){
background-color: $color-blue-lighter;
}
&:nth-child(even){
background-color: $color-white;
}
}
.feat-part{
padding: 1.5rem 0;
background-color: $color-white;
}
.section-feature {
min-height: 6rem;
}
.container{
margin: 0 auto;
height:100%;
position: relative;
max-width: 12.2rem;
}
.feat-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
.feat-img {
height: 1.2rem;
width: 1.2rem;
border-radius: 100%;
margin-bottom: .2rem;
color: $color-white;
i {
line-height: 1.2rem;
font-size: .6rem;
}
}
.feat-title {
font-size: .16rem;
line-height: .3rem;
}
&:first-child {
.feat-img {
background-color: rgb(248, 88, 96);
}
}
&:nth-child(2) {
.feat-img {
background-color: #f9bb13;
}
}
&:nth-child(3) {
.feat-img {
background-color: #20ab8e;
}
}
&:nth-child(4) {
.feat-img {
background-color: rgb(66, 165, 245);
}
}
}
.img-container{
width: 100%;
position: absolute;
top: .74rem;
left: 50%;
transform: translateX(-50%);
text-align: right;
.img{
width: 7.12rem;
border-radius: 4px;
box-shadow : 0 30px 60px rgba(0,0,0,0.2);
}
}
.m-skew {
position: relative;
.skew-container {
padding: 0 1rem;
}
.m-skew-bg {
position: absolute;
left: 0;
top: 5%;
height: 600px;
width: 100%;
transform: skewY(-11deg);
background-image: linear-gradient(180deg, #93a5cf 0%, #e4efe9 100%);
.m-bg-mask {
position: absolute;
height: 200px;
}
.m-bg-mask0 {
bottom: 0;
left: 0;
width: 30%;
background-image: linear-gradient(120deg, #6ab3fd 0%, #c1cfde 102%);
}
.m-bg-mask1 {
bottom: 200px;
right: 0;
width: 36%;
background-image: linear-gradient(219deg, #84a1ce 0%, #e4efe9 100%);
}
.m-bg-mask2 {
top: 0;
left: 0;
width: 30%;
background-image: linear-gradient(219deg, #93a5cf 0%, #d7e3e5 100%);
}
}
}
}
.home-section {
text-align: center;
-webkit-font-smoothing: antialiased;
.title {
color: $color-text-dark;
line-height: .32rem;
margin-bottom: .08rem;
font-size: .24rem;
}
.desc {
color: $color-text-light;
font-size: .16rem;
}
.section-item {
text-align: left;
padding: .24rem;
.img {
width: .48rem;
height: .48rem;
background-image: linear-gradient(-20deg, #21d4fd 0%, #b721ff 100%);
border-radius: 50%;
text-align: center;
line-height: .48rem;
font-size: .24rem;
color: #fff;
margin-bottom: .24rem;
}
.title {
color: $color-text-dark;
font-size: .2rem;
}
.desc {
color: $color-text-light;
font-size: .16rem;
}
}
.row-card {
margin-top: .48rem;
padding: 0 .24rem;
}
.section-card {
padding-bottom: 1rem;
.ant-card {
font-size: .17rem;
border-radius: .04rem;
box-shadow: 0 16px 35px rgba(50,50,93,.1), 0 5px 16px rgba(0,0,0,.07);
border: none;
}
.ant-card:not(.ant-card-no-hovering):hover {
box-shadow: 0 16px 35px rgba(50,50,93,.1), 0 5px 16px rgba(0,0,0,.07);
}
.ant-card-head {
background-color: $color-blue-grey-lighter;
border-top-left-radius: .04rem;
border-top-right-radius: .04rem;
}
.ant-card-head-title {
font-size: .17rem;
color: $color-text-dark;
}
.ant-card-body {
text-align: left;
padding: 0;
}
.mock-desc {
padding: .32rem;
min-height: 8em;
}
padding: .08rem;
}
.code {
color: $color-text-light;
background-color: $color-blue-grey-lighter;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
.orderNum {
background-color: $color-bg-lightblue;
display: inline-block;
text-align: center;
width: .4rem;
margin-right: .5em;
user-select: none;
}
.orderNum-first {
padding-top: .5em;
}
.orderNum-last {
border-bottom-left-radius: 4px;
padding-bottom: .5em;
}
.string {
color: #ff561b;
}
.number {
color: #57cf27;
}
.keyword {
color: #2359f1;
}
.item {
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
}
.mock-after {
.ant-card-head {
background-color: $color-bg-lightblue;
}
.ant-card-head-title {
color: #4074af;
}
}
}
.section-manage {
.section-card{
padding-top: .24rem;
.ant-card {
border-radius: .04rem;
}
.ant-card-body {
padding: 0;
}
}
.section-block {
padding: .24rem;
.item {
font-size: .14rem;
}
}
.block-first {
background-color: #5f48fe;
border-top-left-radius: .04rem;
border-top-right-radius: .04rem;
}
.block-second {
background-color: #5f79fe;
}
.block-third {
background-color: #3ab1f9;
border-bottom-left-radius: .04rem;
border-bottom-right-radius: .04rem;
}
.ant-card-body, h4 {
color: #fff;
}
.manage-word {
text-align: left;
padding-left: .48rem;
.icon {
width: .72rem;
height: .72rem;
line-height: .72rem;
text-align: center;
background-color: #5f48fe;
border-radius: 50%;
font-size: .4rem;
color: #fff;
margin-bottom: .24rem;
}
.desc {
margin-bottom: .16rem;
}
}
}
.row-tip {
margin-top: .48rem;
padding-top: .48rem;
padding-bottom: .24rem;
background-color: #ececec;
.container {
display: flex;
align-items: center;
max-width: 12.2rem;
.tip-title {
flex: 2;
}
.tip-btns {
flex: 1;
}
}
.btn-group {
white-space: nowrap;
}
.title {
-webkit-font-smoothing: antialiased;
padding-left: .24rem;
color: #2e2e5a;
line-height: .32rem;
margin-bottom: .08rem;
font-size: .24rem;
}
.desc {
-webkit-font-smoothing: antialiased;
padding-left: .24rem;
color: #6d7c90;
font-size: .16rem;
}
}
// LOGO 旋转动画
@keyframes spin
{
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (max-width: 1200px) {
.home-header, .home-des {
text-align: center;
}
.home-main .main-one .home-des .logo {
justify-content: center;
}
}

151
vendors/client/containers/Login/Login.js vendored Executable file
View File

@ -0,0 +1,151 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Form, Button, Input, Icon, message, Radio } from 'antd';
import { loginActions, loginLdapActions } from '../../reducer/modules/user';
import { withRouter } from 'react-router';
const FormItem = Form.Item;
const RadioGroup = Radio.Group;
import './Login.scss';
const formItemStyle = {
marginBottom: '.16rem'
};
const changeHeight = {
height: '.42rem'
};
@connect(
state => {
return {
loginData: state.user,
isLDAP: state.user.isLDAP
};
},
{
loginActions,
loginLdapActions
}
)
@withRouter
class Login extends Component {
constructor(props) {
super(props);
this.state = {
loginType: 'ldap'
};
}
static propTypes = {
form: PropTypes.object,
history: PropTypes.object,
loginActions: PropTypes.func,
loginLdapActions: PropTypes.func,
isLDAP: PropTypes.bool
};
handleSubmit = e => {
e.preventDefault();
const form = this.props.form;
form.validateFields((err, values) => {
if (!err) {
if (this.props.isLDAP && this.state.loginType === 'ldap') {
this.props.loginLdapActions(values).then(res => {
if (res.payload.data.errcode == 0) {
this.props.history.replace('/group');
message.success('登录成功! ');
}
});
} else {
this.props.loginActions(values).then(res => {
if (res.payload.data.errcode == 0) {
this.props.history.replace('/group');
message.success('登录成功! ');
}
});
}
}
});
};
componentDidMount() {
//Qsso.attach('qsso-login','/api/user/login_by_token')
console.log('isLDAP', this.props.isLDAP);
}
handleFormLayoutChange = e => {
this.setState({ loginType: e.target.value });
};
render() {
const { getFieldDecorator } = this.props.form;
const { isLDAP } = this.props;
const emailRule =
this.state.loginType === 'ldap'
? {}
: {
required: true,
message: '请输入正确的email!',
pattern: /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{1,})+$/
};
return (
<Form onSubmit={this.handleSubmit}>
{/* 登录类型 (普通登录LDAP登录) */}
{isLDAP && (
<FormItem>
<RadioGroup defaultValue="ldap" onChange={this.handleFormLayoutChange}>
<Radio value="ldap">LDAP</Radio>
<Radio value="normal">普通登录</Radio>
</RadioGroup>
</FormItem>
)}
{/* 用户名 (Email) */}
<FormItem style={formItemStyle}>
{getFieldDecorator('email', { rules: [emailRule] })(
<Input
style={changeHeight}
prefix={<Icon type="user" style={{ fontSize: 13 }} />}
placeholder="Email"
/>
)}
</FormItem>
{/* 密码 */}
<FormItem style={formItemStyle}>
{getFieldDecorator('password', {
rules: [{ required: true, message: '请输入密码!' }]
})(
<Input
style={changeHeight}
prefix={<Icon type="lock" style={{ fontSize: 13 }} />}
type="password"
placeholder="Password"
/>
)}
</FormItem>
{/* 登录按钮 */}
<FormItem style={formItemStyle}>
<Button
style={changeHeight}
type="primary"
htmlType="submit"
className="login-form-button"
>
登录
</Button>
</FormItem>
{/* <div className="qsso-breakline">
<span className="qsso-breakword"></span>
</div>
<Button style={changeHeight} id="qsso-login" type="primary" className="login-form-button" size="large" ghost>QSSO登录</Button> */}
</Form>
);
}
}
const LoginForm = Form.create()(Login);
export default LoginForm;

64
vendors/client/containers/Login/Login.scss vendored Executable file
View File

@ -0,0 +1,64 @@
@import '../../styles/common.scss';
// .login-body {
// background-color: #fff;
// }
.login-container {
padding-bottom: .6rem;
}
.login-form-button {
background-image: linear-gradient(to right, #6d69fe 0%, #48a0fa 100%) !important;
border: none !important;
margin-top: .2rem;
width: 100%;
}
.ant-form-item {
margin-bottom: .1rem;
}
.qsso-breakline{
display: flex;
align-items: center;
color: #6d7c90;
margin: .2rem auto;
&:before, &:after{
content: "";
display: inline-block;
height: .02rem;
flex: 1;
border-top: .01rem solid #6d7c90;
}
.qsso-breakword{
padding: 0 .1rem;
}
}
.card-login {
margin-top: 1.6rem;
margin-bottom: 1.6rem;
border-radius: .04rem;
position: relative;
.login-logo {
font-size: 0;
position: absolute;
left: 50%;
top: 0;
background-image: linear-gradient(-20deg, #21d4fd 0%, #b721ff 100%);
transform: translate(-50%, -50%);
padding: .16rem;
border-radius: 50%;
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
}
.login-title {
text-align: center;
padding-top: .5rem;
font-size: .4rem;
font-weight: 200;
color: #2e2e5a;
}
.svg {
animation: spin 5s linear infinite;
}
}

View File

@ -0,0 +1,36 @@
import React, { PureComponent as Component } from 'react';
import Login from './LoginWrap';
import { Row, Col, Card } from 'antd';
import LogoSVG from '../../components/LogoSVG/index.js';
class LoginContainer extends Component {
render() {
return (
<div className="g-body login-body">
<div className="m-bg">
<div className="m-bg-mask m-bg-mask0" />
<div className="m-bg-mask m-bg-mask1" />
<div className="m-bg-mask m-bg-mask2" />
<div className="m-bg-mask m-bg-mask3" />
</div>
<div className="main-one login-container">
<div className="container">
<Row type="flex" justify="center">
<Col xs={20} sm={16} md={12} lg={8} className="container-login">
<Card className="card-login">
<h2 className="login-title">YAPI</h2>
<div className="login-logo">
<LogoSVG length="100px" />
</div>
<Login />
</Card>
</Col>
</Row>
</div>
</div>
</div>
);
}
}
export default LoginContainer;

43
vendors/client/containers/Login/LoginWrap.js vendored Executable file
View File

@ -0,0 +1,43 @@
import React, { PureComponent as Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Tabs } from 'antd';
import LoginForm from './Login';
import RegForm from './Reg';
import './Login.scss';
const TabPane = Tabs.TabPane;
@connect(state => ({
loginWrapActiveKey: state.user.loginWrapActiveKey,
canRegister: state.user.canRegister
}))
export default class LoginWrap extends Component {
constructor(props) {
super(props);
}
static propTypes = {
form: PropTypes.object,
loginWrapActiveKey: PropTypes.string,
canRegister: PropTypes.bool
};
render() {
const { loginWrapActiveKey, canRegister } = this.props;
{/** show only login when register is disabled */}
return (
<Tabs
defaultActiveKey={loginWrapActiveKey}
className="login-form"
tabBarStyle={{ border: 'none' }}
>
<TabPane tab="登录" key="1">
<LoginForm />
</TabPane>
<TabPane tab={"注册"} key="2">
{canRegister ? <RegForm /> : <div style={{minHeight: 200}}>管理员已禁止注册请联系管理员</div>}
</TabPane>
</Tabs>
);
}
}

169
vendors/client/containers/Login/Reg.js vendored Executable file
View File

@ -0,0 +1,169 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Form, Button, Input, Icon, message } from 'antd';
import { regActions } from '../../reducer/modules/user';
import { withRouter } from 'react-router';
const FormItem = Form.Item;
const formItemStyle = {
marginBottom: '.16rem'
};
const changeHeight = {
height: '.42rem'
};
@connect(
state => {
return {
loginData: state.user
};
},
{
regActions
}
)
@withRouter
class Reg extends Component {
constructor(props) {
super(props);
this.state = {
confirmDirty: false
};
}
static propTypes = {
form: PropTypes.object,
history: PropTypes.object,
regActions: PropTypes.func
};
handleSubmit = e => {
e.preventDefault();
const form = this.props.form;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
this.props.regActions(values).then(res => {
if (res.payload.data.errcode == 0) {
this.props.history.replace('/group');
message.success('注册成功! ');
}
});
}
});
};
checkPassword = (rule, value, callback) => {
const form = this.props.form;
if (value && value !== form.getFieldValue('password')) {
callback('两次输入的密码不一致啊!');
} else {
callback();
}
};
checkConfirm = (rule, value, callback) => {
const form = this.props.form;
if (value && this.state.confirmDirty) {
form.validateFields(['confirm'], { force: true });
}
callback();
};
render() {
const { getFieldDecorator } = this.props.form;
return (
<Form onSubmit={this.handleSubmit}>
{/* 用户名 */}
<FormItem style={formItemStyle}>
{getFieldDecorator('userName', {
rules: [{ required: true, message: '请输入用户名!' }]
})(
<Input
style={changeHeight}
prefix={<Icon type="user" style={{ fontSize: 13 }} />}
placeholder="Username"
/>
)}
</FormItem>
{/* Emaiil */}
<FormItem style={formItemStyle}>
{getFieldDecorator('email', {
rules: [
{
required: true,
message: '请输入email!',
pattern: /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{1,})+$/
}
]
})(
<Input
style={changeHeight}
prefix={<Icon type="mail" style={{ fontSize: 13 }} />}
placeholder="Email"
/>
)}
</FormItem>
{/* 密码 */}
<FormItem style={formItemStyle}>
{getFieldDecorator('password', {
rules: [
{
required: true,
message: '请输入密码!'
},
{
validator: this.checkConfirm
}
]
})(
<Input
style={changeHeight}
prefix={<Icon type="lock" style={{ fontSize: 13 }} />}
type="password"
placeholder="Password"
/>
)}
</FormItem>
{/* 密码二次确认 */}
<FormItem style={formItemStyle}>
{getFieldDecorator('confirm', {
rules: [
{
required: true,
message: '请再次输入密码密码!'
},
{
validator: this.checkPassword
}
]
})(
<Input
style={changeHeight}
prefix={<Icon type="lock" style={{ fontSize: 13 }} />}
type="password"
placeholder="Confirm Password"
/>
)}
</FormItem>
{/* 注册按钮 */}
<FormItem style={formItemStyle}>
<Button
style={changeHeight}
type="primary"
htmlType="submit"
className="login-form-button"
>
注册
</Button>
</FormItem>
</Form>
);
}
}
const RegForm = Form.create()(Reg);
export default RegForm;

80
vendors/client/containers/News/News.js vendored Executable file
View File

@ -0,0 +1,80 @@
import './News.scss';
import React, { PureComponent as Component } from 'react';
import NewsTimeline from './NewsTimeline/NewsTimeline';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Breadcrumb from '../../components/Breadcrumb/Breadcrumb';
import { Button } from 'antd';
import { getMockUrl } from '../../reducer/modules/news.js';
import Subnav from '../../components/Subnav/Subnav.js';
@connect(
state => {
return {
uid: state.user.uid + ''
};
},
{
getMockUrl: getMockUrl
}
)
class News extends Component {
constructor(props) {
super(props);
this.state = {
mockURL: ''
};
}
static propTypes = {
uid: PropTypes.string,
getMockUrl: PropTypes.func
};
componentWillMount() {
//const that = this;
// this.props.getMockUrl(2724).then(function(data){
// const { prd_host, basepath, protocol } = data.payload.data.data;
// const mockURL = `${protocol}://${prd_host}${basepath}/{path}`;
// that.setState({
// mockURL: mockURL
// })
// })
}
render() {
return (
<div>
<Subnav
default={'动态'}
data={[
{
name: '动态',
path: '/news'
},
{
name: '测试',
path: '/follow'
},
{
name: '设置',
path: '/follow'
}
]}
/>
<div className="g-row">
<section className="news-box m-panel">
<div className="logHead">
<Breadcrumb />
<div className="Mockurl">
<span>Mock地址</span>
<p>{this.state.mockURL}</p>
<Button type="primary">下载Mock数据</Button>
</div>
</div>
<NewsTimeline />
</section>
</div>
</div>
);
}
}
export default News;

101
vendors/client/containers/News/News.scss vendored Executable file
View File

@ -0,0 +1,101 @@
@import '../../styles/mixin.scss';
.news-box {
@include row-width-limit;
display: -webkit-box;
-webkit-box-flex: 1;
margin: 0px auto 0 auto;
font-size: 0.14rem;
background: #FFF;
display: block;
.news-timeline{
padding: 24px;
padding-left: 125px;
color: #6b6c6d;
.ant-timeline-item{
min-height: 60px;
}
.ant-timeline-item-head{
width: 30px;
height: 30px;
left: -8px;
top: -4px;
border-color:#e1e3e4;
}
.logusername{
color: #4eaef3;
padding: 0px 16px 0px 8px;
cursor: pointer;
}
.logtype{
padding-right: 16px;
}
.logtime{
padding-right: 16px;
}
.logcontent{
display: block;
padding-left: 8px;
line-height: 24px;
}
.logoTimeago{
position: absolute;
left: -80px;
top: 5px;
color: #c0c1c1;
}
.logbidden{
color: #c0c1c1;
cursor: default;
line-height: 30px;
padding-left: 30px;
}
.loggetMore{
line-height: 30px;
padding-left: 30px;
color: #4eaef3;
}
}
.logHead{
height: 80px;
width: 100%;
border-bottom: 1px solid #e9e9e9;
padding: 24px 0px;
overflow: hidden;
.breadcrumb-container{
float: left;
min-width:100px;
}
.Mockurl{
width: 500px;
float: right;
color: #7b7b7b;
>span{
float: left;
line-height: 30px;
}
p{
width: 60%;
display: inline-block;
position: relative;
padding: 4px 7px;
height: 28px;
cursor: text;
font-size: 13px;
color: rgba(0,0,0,.65);
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
-webkit-transition: all .3s;
transition: all .3s;
overflow-x:auto;
}
button{
float: right;
}
}
}
}

View File

@ -0,0 +1,80 @@
import React, { PureComponent as Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Menu } from 'antd';
import { fetchNewsData } from '../../../reducer/modules/news.js';
const logList = [
{
name: '用户'
},
{
name: '分组'
},
{
name: '接口'
},
{
name: '项目'
}
];
@connect(
state => {
// console.log(state);
return {
uid: state.user.uid + '',
newsData: state.news.newsData
};
},
{
fetchNewsData
}
)
class NewsList extends Component {
static propTypes = {
fetchNewsData: PropTypes.func,
setLoading: PropTypes.func,
uid: PropTypes.string
};
constructor(props) {
super(props);
this.state = {
selectedKeys: 0
};
}
getLogData(e) {
// page,size,logId
// console.log(e.key);
this.setState({
selectedKeys: +e.key
});
const that = this;
this.props.setLoading(true);
this.props.fetchNewsData(+this.props.uid, 0, 5).then(function() {
that.props.setLoading(false);
});
}
render() {
return (
<div className="logList">
<h3>日志类型</h3>
<Menu
mode="inline"
selectedKeys={[`${this.state.selectedKeys}`]}
onClick={this.getLogData.bind(this)}
>
{logList.map((item, i) => {
return (
<Menu.Item key={i} className="log-item">
{item.name}
</Menu.Item>
);
})}
</Menu>
</div>
);
}
}
export default NewsList;

View File

@ -0,0 +1,89 @@
import React, { PureComponent as Component } from 'react';
import { Timeline, Spin } from 'antd';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { formatTime } from '../../../common.js';
import { fetchNewsData } from '../../../reducer/modules/news.js';
import { timeago } from '../../../../common/utils';
// timeago(new Date().getTime() - 40);
@connect(
state => {
return {
newsData: state.news.newsData,
curpage: state.news.curpage
};
},
{
fetchNewsData: fetchNewsData
}
)
class NewsTimeline extends Component {
static propTypes = {
newsData: PropTypes.object,
fetchNewsData: PropTypes.func,
setLoading: PropTypes.func,
loading: PropTypes.bool,
curpage: PropTypes.number
};
constructor(props) {
super(props);
this.state = {
bidden: '',
loading: false
};
}
getMore() {
const that = this;
this.setState({ loading: true });
this.props.fetchNewsData(21, 'project', this.props.curpage, 8).then(function() {
that.setState({ loading: false });
if (that.props.newsData.total + 1 === that.props.curpage) {
that.setState({ bidden: 'logbidden' });
}
});
}
componentWillMount() {
this.props.fetchNewsData(21, 'project', this.props.curpage, 8);
}
render() {
let data = this.props.newsData ? this.props.newsData.list : [];
if (data && data.length) {
data = data.map(function(item, i) {
return (
<Timeline.Item key={i}>
<span className="logoTimeago">{timeago(item.add_time)}</span>
<span className="logusername">{item.username}</span>
<span className="logtype">{item.type}</span>
<span className="logtime">{formatTime(item.add_time)}</span>
<span className="logcontent">{item.content}</span>
</Timeline.Item>
);
});
} else {
data = '';
}
let pending = this.state.bidden ? (
<a className={this.state.bidden}>以上为全部内容</a>
) : (
<a className="loggetMore" onClick={this.getMore.bind(this)}>
查看更多
</a>
);
if (this.state.loading) {
pending = <Spin />;
}
return (
<section className="news-timeline">
{data ? <Timeline pending={pending}>{data}</Timeline> : data}
</section>
);
}
}
export default NewsTimeline;

View File

@ -0,0 +1,58 @@
import './Activity.scss';
import React, { PureComponent as Component } from 'react';
import TimeTree from '../../../components/TimeLine/TimeLine';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Button } from 'antd';
@connect(state => {
return {
uid: state.user.uid + '',
curdata: state.inter.curdata,
currProject: state.project.currProject
};
})
class Activity extends Component {
constructor(props) {
super(props);
}
static propTypes = {
uid: PropTypes.string,
getMockUrl: PropTypes.func,
match: PropTypes.object,
curdata: PropTypes.object,
currProject: PropTypes.object
};
render() {
let { currProject } = this.props;
return (
<div className="g-row">
<section className="news-box m-panel">
<div style={{ display: 'none' }} className="logHead">
{/*<Breadcrumb />*/}
<div className="projectDes">
<p>高效易用可部署的API管理平台</p>
</div>
<div className="Mockurl">
<span>Mock地址</span>
<p>
{location.protocol +
'//' +
location.hostname +
(location.port !== '' ? ':' + location.port : '') +
`/mock/${currProject._id}${currProject.basepath}/yourPath`}
</p>
<Button type="primary">
<a href={`/api/project/download?project_id=${this.props.match.params.id}`}>
下载Mock数据
</a>
</Button>
</div>
</div>
<TimeTree type={'project'} typeid={+this.props.match.params.id} />
</section>
</div>
);
}
}
export default Activity;

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,141 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { Tabs, Layout } from 'antd';
import { Route, Switch, matchPath } from 'react-router-dom';
import { connect } from 'react-redux';
const { Content, Sider } = Layout;
import './interface.scss';
import InterfaceMenu from './InterfaceList/InterfaceMenu.js';
import InterfaceList from './InterfaceList/InterfaceList.js';
import InterfaceContent from './InterfaceList/InterfaceContent.js';
import InterfaceColMenu from './InterfaceCol/InterfaceColMenu.js';
import InterfaceColContent from './InterfaceCol/InterfaceColContent.js';
import InterfaceCaseContent from './InterfaceCol/InterfaceCaseContent.js';
import { getProject } from '../../../reducer/modules/project';
import { setColData } from '../../../reducer/modules/interfaceCol.js';
const contentRouter = {
path: '/project/:id/interface/:action/:actionId',
exact: true
};
const InterfaceRoute = props => {
let C;
if (props.match.params.action === 'api') {
if (!props.match.params.actionId) {
C = InterfaceList;
} else if (!isNaN(props.match.params.actionId)) {
C = InterfaceContent;
} else if (props.match.params.actionId.indexOf('cat_') === 0) {
C = InterfaceList;
}
} else if (props.match.params.action === 'col') {
C = InterfaceColContent;
} else if (props.match.params.action === 'case') {
C = InterfaceCaseContent;
} else {
const params = props.match.params;
props.history.replace('/project/' + params.id + '/interface/api');
return null;
}
return <C {...props} />;
};
InterfaceRoute.propTypes = {
match: PropTypes.object,
history: PropTypes.object
};
@connect(
state => {
return {
isShowCol: state.interfaceCol.isShowCol
};
},
{
setColData,
getProject
}
)
class Interface extends Component {
static propTypes = {
match: PropTypes.object,
history: PropTypes.object,
location: PropTypes.object,
isShowCol: PropTypes.bool,
getProject: PropTypes.func,
setColData: PropTypes.func
// fetchInterfaceColList: PropTypes.func
};
constructor(props) {
super(props);
// this.state = {
// curkey: this.props.match.params.action === 'api' ? 'api' : 'colOrCase'
// }
}
onChange = action => {
let params = this.props.match.params;
if (action === 'colOrCase') {
action = this.props.isShowCol ? 'col' : 'case';
}
this.props.history.push('/project/' + params.id + '/interface/' + action);
};
async componentWillMount() {
this.props.setColData({
isShowCol: true
});
// await this.props.fetchInterfaceColList(this.props.match.params.id)
}
render() {
const { action } = this.props.match.params;
// const activeKey = this.state.curkey;
const activeKey = action === 'api' ? 'api' : 'colOrCase';
return (
<Layout style={{ minHeight: 'calc(100vh - 156px)', marginLeft: '24px', marginTop: '24px' }}>
<Sider style={{ height: '100%' }} width={300}>
<div className="left-menu">
<Tabs type="card" className="tabs-large" activeKey={activeKey} onChange={this.onChange}>
<Tabs.TabPane tab="接口列表" key="api" />
<Tabs.TabPane tab="测试集合" key="colOrCase" />
</Tabs>
{activeKey === 'api' ? (
<InterfaceMenu
router={matchPath(this.props.location.pathname, contentRouter)}
projectId={this.props.match.params.id}
/>
) : (
<InterfaceColMenu
router={matchPath(this.props.location.pathname, contentRouter)}
projectId={this.props.match.params.id}
/>
)}
</div>
</Sider>
<Layout>
<Content
style={{
height: '100%',
margin: '0 24px 0 16px',
overflow: 'initial',
backgroundColor: '#fff'
}}
>
<div className="right-content">
<Switch>
<Route exact path="/project/:id/interface/:action" component={InterfaceRoute} />
<Route {...contentRouter} component={InterfaceRoute} />
</Switch>
</div>
</Content>
</Layout>
</Layout>
);
}
}
export default Interface;

View File

@ -0,0 +1,125 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Row, Col, Tabs } from 'antd';
const TabPane = Tabs.TabPane;
function jsonFormat(json) {
// console.log('json',json)
if (json && typeof json === 'object') {
return JSON.stringify(json, null, ' ');
}
return json;
}
const CaseReport = function(props) {
let params = jsonFormat(props.data);
let headers = jsonFormat(props.headers, null, ' ');
let res_header = jsonFormat(props.res_header, null, ' ');
let res_body = jsonFormat(props.res_body);
let httpCode = props.status;
let validRes;
if (props.validRes && Array.isArray(props.validRes)) {
validRes = props.validRes.map((item, index) => {
return <div key={index}>{item.message}</div>;
});
}
return (
<div className="report">
<Tabs defaultActiveKey="request">
<TabPane className="case-report-pane" tab="Request" key="request">
<Row className="case-report">
<Col className="case-report-title" span="6">
Url
</Col>
<Col span="18">{props.url}</Col>
</Row>
{props.query ? (
<Row className="case-report">
<Col className="case-report-title" span="6">
Query
</Col>
<Col span="18">{props.query}</Col>
</Row>
) : null}
{props.headers ? (
<Row className="case-report">
<Col className="case-report-title" span="6">
Headers
</Col>
<Col span="18">
<pre>{headers}</pre>
</Col>
</Row>
) : null}
{params ? (
<Row className="case-report">
<Col className="case-report-title" span="6">
Body
</Col>
<Col span="18">
<pre style={{ whiteSpace: 'pre-wrap' }}>{params}</pre>
</Col>
</Row>
) : null}
</TabPane>
<TabPane className="case-report-pane" tab="Response" key="response">
<Row className="case-report">
<Col className="case-report-title" span="6">
HttpCode
</Col>
<Col span="18">
<pre>{httpCode}</pre>
</Col>
</Row>
{props.res_header ? (
<Row className="case-report">
<Col className="case-report-title" span="6">
Headers
</Col>
<Col span="18">
<pre>{res_header}</pre>
</Col>
</Row>
) : null}
{props.res_body ? (
<Row className="case-report">
<Col className="case-report-title" span="6">
Body
</Col>
<Col span="18">
<pre>{res_body}</pre>
</Col>
</Row>
) : null}
</TabPane>
<TabPane className="case-report-pane" tab="验证结果" key="valid">
{props.validRes ? (
<Row className="case-report">
<Col className="case-report-title" span="6">
验证结果
</Col>
<Col span="18"><pre>
{validRes}
</pre></Col>
</Row>
) : null}
</TabPane>
</Tabs>
</div>
);
};
CaseReport.propTypes = {
url: PropTypes.string,
data: PropTypes.any,
headers: PropTypes.object,
res_header: PropTypes.object,
res_body: PropTypes.any,
query: PropTypes.string,
validRes: PropTypes.array,
status: PropTypes.number
};
export default CaseReport;

View File

@ -0,0 +1,241 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { Table, Select, Tooltip, Icon } from 'antd';
import variable from '../../../../constants/variable';
import { connect } from 'react-redux';
const Option = Select.Option;
import { fetchInterfaceListMenu } from '../../../../reducer/modules/interface.js';
@connect(
state => {
return {
projectList: state.project.projectList,
list: state.inter.list
};
},
{
fetchInterfaceListMenu
}
)
export default class ImportInterface extends Component {
constructor(props) {
super(props);
}
state = {
selectedRowKeys: [],
categoryCount: {},
project: this.props.currProjectId
};
static propTypes = {
list: PropTypes.array,
selectInterface: PropTypes.func,
projectList: PropTypes.array,
currProjectId: PropTypes.string,
fetchInterfaceListMenu: PropTypes.func
};
async componentDidMount() {
// console.log(this.props.currProjectId)
await this.props.fetchInterfaceListMenu(this.props.currProjectId);
}
// 切换项目
onChange = async val => {
this.setState({
project: val,
selectedRowKeys: [],
categoryCount: {}
});
await this.props.fetchInterfaceListMenu(val);
};
render() {
const { list, projectList } = this.props;
// const { selectedRowKeys } = this.state;
const data = list.map(item => {
return {
key: 'category_' + item._id,
title: item.name,
isCategory: true,
children: item.list
? item.list.map(e => {
e.key = e._id;
e.categoryKey = 'category_' + item._id;
e.categoryLength = item.list.length;
return e;
})
: []
};
});
const self = this;
const rowSelection = {
// onChange: (selectedRowKeys) => {
// console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
// if (selectedRows.isCategory) {
// const selectedRowKeys = selectedRows.children.map(item => item._id)
// this.setState({ selectedRowKeys })
// }
// this.props.onChange(selectedRowKeys.filter(id => ('' + id).indexOf('category') === -1));
// },
onSelect: (record, selected) => {
// console.log(record, selected, selectedRows);
const oldSelecteds = self.state.selectedRowKeys;
const categoryCount = self.state.categoryCount;
const categoryKey = record.categoryKey;
const categoryLength = record.categoryLength;
let selectedRowKeys = [];
if (record.isCategory) {
selectedRowKeys = record.children.map(item => item._id).concat(record.key);
if (selected) {
selectedRowKeys = selectedRowKeys
.filter(id => oldSelecteds.indexOf(id) === -1)
.concat(oldSelecteds);
categoryCount[categoryKey] = categoryLength;
} else {
selectedRowKeys = oldSelecteds.filter(id => selectedRowKeys.indexOf(id) === -1);
categoryCount[categoryKey] = 0;
}
} else {
if (selected) {
selectedRowKeys = oldSelecteds.concat(record._id);
if (categoryCount[categoryKey]) {
categoryCount[categoryKey] += 1;
} else {
categoryCount[categoryKey] = 1;
}
if (categoryCount[categoryKey] === record.categoryLength) {
selectedRowKeys.push(categoryKey);
}
} else {
selectedRowKeys = oldSelecteds.filter(id => id !== record._id);
if (categoryCount[categoryKey]) {
categoryCount[categoryKey] -= 1;
}
selectedRowKeys = selectedRowKeys.filter(id => id !== categoryKey);
}
}
self.setState({ selectedRowKeys, categoryCount });
self.props.selectInterface(
selectedRowKeys.filter(id => ('' + id).indexOf('category') === -1),
self.state.project
);
},
onSelectAll: selected => {
// console.log(selected, selectedRows, changeRows);
let selectedRowKeys = [];
let categoryCount = self.state.categoryCount;
if (selected) {
data.forEach(item => {
if (item.children) {
categoryCount['category_' + item._id] = item.children.length;
selectedRowKeys = selectedRowKeys.concat(item.children.map(item => item._id));
}
});
selectedRowKeys = selectedRowKeys.concat(data.map(item => item.key));
} else {
categoryCount = {};
selectedRowKeys = [];
}
self.setState({ selectedRowKeys, categoryCount });
self.props.selectInterface(
selectedRowKeys.filter(id => ('' + id).indexOf('category') === -1),
self.state.project
);
},
selectedRowKeys: self.state.selectedRowKeys
};
const columns = [
{
title: '接口名称',
dataIndex: 'title',
width: '30%'
},
{
title: '接口路径',
dataIndex: 'path',
width: '40%'
},
{
title: '请求方法',
dataIndex: 'method',
render: item => {
let methodColor = variable.METHOD_COLOR[item ? item.toLowerCase() : 'get'];
return (
<span
style={{
color: methodColor.color,
backgroundColor: methodColor.bac,
borderRadius: 4
}}
className="colValue"
>
{item}
</span>
);
}
},
{
title: (
<span>
状态{' '}
<Tooltip title="筛选满足条件的接口集合">
<Icon type="question-circle-o" />
</Tooltip>
</span>
),
dataIndex: 'status',
render: text => {
return (
text &&
(text === 'done' ? (
<span className="tag-status done">已完成</span>
) : (
<span className="tag-status undone">未完成</span>
))
);
},
filters: [
{
text: '已完成',
value: 'done'
},
{
text: '未完成',
value: 'undone'
}
],
onFilter: (value, record) => {
let arr = record.children.filter(item => {
return item.status.indexOf(value) === 0;
});
return arr.length > 0;
// record.status.indexOf(value) === 0
}
}
];
return (
<div>
<div className="select-project">
<span>选择要导入的项目 </span>
<Select value={this.state.project} style={{ width: 200 }} onChange={this.onChange}>
{projectList.map(item => {
return item.projectname ? (
''
) : (
<Option value={`${item._id}`} key={item._id}>
{item.name}
</Option>
);
})}
</Select>
</div>
<Table columns={columns} rowSelection={rowSelection} dataSource={data} pagination={false} />
</div>
);
}
}

View File

@ -0,0 +1,235 @@
import React, { PureComponent as Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import { Link } from 'react-router-dom';
import axios from 'axios';
import { message, Tooltip, Input } from 'antd';
import { getEnv } from '../../../../reducer/modules/project';
import {
fetchInterfaceColList,
setColData,
fetchCaseData,
fetchCaseList
} from '../../../../reducer/modules/interfaceCol';
import { Postman } from '../../../../components';
import './InterfaceCaseContent.scss';
@connect(
state => {
return {
interfaceColList: state.interfaceCol.interfaceColList,
currColId: state.interfaceCol.currColId,
currCaseId: state.interfaceCol.currCaseId,
currCase: state.interfaceCol.currCase,
isShowCol: state.interfaceCol.isShowCol,
currProject: state.project.currProject,
projectEnv: state.project.projectEnv,
curUid: state.user.uid
};
},
{
fetchInterfaceColList,
fetchCaseData,
setColData,
fetchCaseList,
getEnv
}
)
@withRouter
export default class InterfaceCaseContent extends Component {
static propTypes = {
match: PropTypes.object,
interfaceColList: PropTypes.array,
fetchInterfaceColList: PropTypes.func,
fetchCaseData: PropTypes.func,
setColData: PropTypes.func,
fetchCaseList: PropTypes.func,
history: PropTypes.object,
currColId: PropTypes.number,
currCaseId: PropTypes.number,
currCase: PropTypes.object,
isShowCol: PropTypes.bool,
currProject: PropTypes.object,
getEnv: PropTypes.func,
projectEnv: PropTypes.object,
curUid: PropTypes.number
};
state = {
isEditingCasename: true,
editCasename: ''
};
constructor(props) {
super(props);
}
getColId(colList, currCaseId) {
let currColId = 0;
colList.forEach(col => {
col.caseList.forEach(caseItem => {
if (+caseItem._id === +currCaseId) {
currColId = col._id;
}
});
});
return currColId;
}
async componentWillMount() {
const result = await this.props.fetchInterfaceColList(this.props.match.params.id);
let { currCaseId } = this.props;
const params = this.props.match.params;
const { actionId } = params;
currCaseId = +actionId || +currCaseId || result.payload.data.data[0].caseList[0]._id;
let currColId = this.getColId(result.payload.data.data, currCaseId);
// this.props.history.push('/project/' + params.id + '/interface/case/' + currCaseId);
await this.props.fetchCaseData(currCaseId);
this.props.setColData({ currCaseId: +currCaseId, currColId, isShowCol: false });
// 获取当前case 下的环境变量
await this.props.getEnv(this.props.currCase.project_id);
// await this.getCurrEnv()
this.setState({ editCasename: this.props.currCase.casename });
}
async componentWillReceiveProps(nextProps) {
const oldCaseId = this.props.match.params.actionId;
const newCaseId = nextProps.match.params.actionId;
const { interfaceColList } = nextProps;
let currColId = this.getColId(interfaceColList, newCaseId);
if (oldCaseId !== newCaseId) {
await this.props.fetchCaseData(newCaseId);
this.props.setColData({ currCaseId: +newCaseId, currColId, isShowCol: false });
await this.props.getEnv(this.props.currCase.project_id);
// await this.getCurrEnv()
this.setState({ editCasename: this.props.currCase.casename });
}
}
savePostmanRef = postman => {
this.postman = postman;
};
updateCase = async () => {
const {
case_env,
req_params,
req_query,
req_headers,
req_body_type,
req_body_form,
req_body_other,
test_script,
enable_script,
test_res_body,
test_res_header
} = this.postman.state;
const { editCasename: casename } = this.state;
const { _id: id } = this.props.currCase;
let params = {
id,
casename,
case_env,
req_params,
req_query,
req_headers,
req_body_type,
req_body_form,
req_body_other,
test_script,
enable_script,
test_res_body,
test_res_header
};
const res = await axios.post('/api/col/up_case', params);
if (this.props.currCase.casename !== casename) {
this.props.fetchInterfaceColList(this.props.match.params.id);
}
if (res.data.errcode) {
message.error(res.data.errmsg);
} else {
message.success('更新成功');
this.props.fetchCaseData(id);
}
};
triggerEditCasename = () => {
this.setState({
isEditingCasename: true,
editCasename: this.props.currCase.casename
});
};
cancelEditCasename = () => {
this.setState({
isEditingCasename: false,
editCasename: this.props.currCase.casename
});
};
render() {
const { currCase, currProject, projectEnv } = this.props;
const { isEditingCasename, editCasename } = this.state;
const data = Object.assign(
{},
currCase,
{
env: projectEnv.env,
pre_script: currProject.pre_script,
after_script: currProject.after_script
},
{ _id: currCase._id }
);
return (
<div style={{ padding: '6px 0' }} className="case-content">
<div className="case-title">
{!isEditingCasename && (
<Tooltip title="点击编辑" placement="bottom">
<div className="case-name" onClick={this.triggerEditCasename}>
{currCase.casename}
</div>
</Tooltip>
)}
{isEditingCasename && (
<div className="edit-case-name">
<Input
value={editCasename}
onChange={e => this.setState({ editCasename: e.target.value })}
style={{ fontSize: 18 }}
/>
</div>
)}
<span className="inter-link" style={{ margin: '0px 8px 0px 6px', fontSize: 12 }}>
<Link
className="text"
to={`/project/${currCase.project_id}/interface/api/${currCase.interface_id}`}
>
对应接口
</Link>
</span>
</div>
<div>
{Object.keys(currCase).length > 0 && (
<Postman
data={data}
type="case"
saveTip="更新保存修改"
save={this.updateCase}
ref={this.savePostmanRef}
interfaceId={currCase.interface_id}
projectId={currCase.project_id}
curUid={this.props.curUid}
/>
)}
</div>
</div>
);
}
}

View File

@ -0,0 +1,33 @@
.case-content {
padding: 6px 0;
.case-title {
display: flex;
.case-name {
margin-left: 8px;
border-radius: 4px;
border: 1px solid transparent;
padding: 0 5px;
background-color: #eee;
font-size: 20px;
flex-grow: 1;
cursor: pointer;
}
.case-name:hover {
color: rgba(0,0,0,.65);
border: 1px solid #d9d9d9;
}
.edit-case-name {
margin-left: 8px;
display: flex;
flex-grow: 1;
}
.inter-link {
flex-basis: 50px;
position: relative;
}
.inter-link .text {
position: absolute;
bottom: 4px;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,630 @@
import React, { PureComponent as Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import PropTypes from 'prop-types';
import {
fetchInterfaceColList,
fetchInterfaceCaseList,
setColData,
fetchCaseList,
fetchCaseData
} from '../../../../reducer/modules/interfaceCol';
import { fetchProjectList } from '../../../../reducer/modules/project';
import axios from 'axios';
import ImportInterface from './ImportInterface';
import { Input, Icon, Button, Modal, message, Tooltip, Tree, Form } from 'antd';
import { arrayChangeIndex } from '../../../../common.js';
import _ from 'underscore'
const TreeNode = Tree.TreeNode;
const FormItem = Form.Item;
const confirm = Modal.confirm;
const headHeight = 240; // menu顶部到网页顶部部分的高度
import './InterfaceColMenu.scss';
const ColModalForm = Form.create()(props => {
const { visible, onCancel, onCreate, form, title } = props;
const { getFieldDecorator } = form;
return (
<Modal visible={visible} title={title} onCancel={onCancel} onOk={onCreate}>
<Form layout="vertical">
<FormItem label="集合名">
{getFieldDecorator('colName', {
rules: [{ required: true, message: '请输入集合命名!' }]
})(<Input />)}
</FormItem>
<FormItem label="简介">{getFieldDecorator('colDesc')(<Input type="textarea" />)}</FormItem>
</Form>
</Modal>
);
});
@connect(
state => {
return {
interfaceColList: state.interfaceCol.interfaceColList,
currCase: state.interfaceCol.currCase,
isRander: state.interfaceCol.isRander,
currCaseId: state.interfaceCol.currCaseId,
// list: state.inter.list,
// 当前项目的信息
curProject: state.project.currProject
// projectList: state.project.projectList
};
},
{
fetchInterfaceColList,
fetchInterfaceCaseList,
fetchCaseData,
// fetchInterfaceListMenu,
fetchCaseList,
setColData,
fetchProjectList
}
)
@withRouter
export default class InterfaceColMenu extends Component {
static propTypes = {
match: PropTypes.object,
interfaceColList: PropTypes.array,
fetchInterfaceColList: PropTypes.func,
fetchInterfaceCaseList: PropTypes.func,
// fetchInterfaceListMenu: PropTypes.func,
fetchCaseList: PropTypes.func,
fetchCaseData: PropTypes.func,
setColData: PropTypes.func,
currCaseId: PropTypes.number,
history: PropTypes.object,
isRander: PropTypes.bool,
// list: PropTypes.array,
router: PropTypes.object,
currCase: PropTypes.object,
curProject: PropTypes.object,
fetchProjectList: PropTypes.func
// projectList: PropTypes.array
};
state = {
colModalType: '',
colModalVisible: false,
editColId: 0,
filterValue: '',
importInterVisible: false,
importInterIds: [],
importColId: 0,
expands: null,
list: [],
delIcon: null,
selectedProject: null
};
constructor(props) {
super(props);
}
componentWillMount() {
this.getList();
}
componentWillReceiveProps(nextProps) {
if (this.props.interfaceColList !== nextProps.interfaceColList) {
this.setState({
list: nextProps.interfaceColList
});
}
}
async getList() {
let r = await this.props.fetchInterfaceColList(this.props.match.params.id);
this.setState({
list: r.payload.data.data
});
return r;
}
addorEditCol = async () => {
const { colName: name, colDesc: desc } = this.form.getFieldsValue();
const { colModalType, editColId: col_id } = this.state;
const project_id = this.props.match.params.id;
let res = {};
if (colModalType === 'add') {
res = await axios.post('/api/col/add_col', { name, desc, project_id });
} else if (colModalType === 'edit') {
res = await axios.post('/api/col/up_col', { name, desc, col_id });
}
if (!res.data.errcode) {
this.setState({
colModalVisible: false
});
message.success(colModalType === 'edit' ? '修改集合成功' : '添加集合成功');
// await this.props.fetchInterfaceColList(project_id);
this.getList();
} else {
message.error(res.data.errmsg);
}
};
onExpand = keys => {
this.setState({ expands: keys });
};
onSelect = _.debounce(keys => {
if (keys.length) {
const type = keys[0].split('_')[0];
const id = keys[0].split('_')[1];
const project_id = this.props.match.params.id;
if (type === 'col') {
this.props.setColData({
isRander: false
});
this.props.history.push('/project/' + project_id + '/interface/col/' + id);
} else {
this.props.setColData({
isRander: false
});
this.props.history.push('/project/' + project_id + '/interface/case/' + id);
}
}
this.setState({
expands: null
});
}, 500);
showDelColConfirm = colId => {
let that = this;
const params = this.props.match.params;
confirm({
title: '您确认删除此测试集合',
content: '温馨提示:该操作会删除该集合下所有测试用例,用例删除后无法恢复',
okText: '确认',
cancelText: '取消',
async onOk() {
const res = await axios.get('/api/col/del_col?col_id=' + colId);
if (!res.data.errcode) {
message.success('删除集合成功');
const result = await that.getList();
const nextColId = result.payload.data.data[0]._id;
that.props.history.push('/project/' + params.id + '/interface/col/' + nextColId);
} else {
message.error(res.data.errmsg);
}
}
});
};
// 复制测试集合
copyInterface = async item => {
if (this._copyInterfaceSign === true) {
return;
}
this._copyInterfaceSign = true;
const { desc, project_id, _id: col_id } = item;
let { name } = item;
name = `${name} copy`;
// 添加集合
const add_col_res = await axios.post('/api/col/add_col', { name, desc, project_id });
if (add_col_res.data.errcode) {
message.error(add_col_res.data.errmsg);
return;
}
const new_col_id = add_col_res.data.data._id;
// 克隆集合
const add_case_list_res = await axios.post('/api/col/clone_case_list', {
new_col_id,
col_id,
project_id
});
this._copyInterfaceSign = false;
if (add_case_list_res.data.errcode) {
message.error(add_case_list_res.data.errmsg);
return;
}
// 刷新接口列表
// await this.props.fetchInterfaceColList(project_id);
this.getList();
this.props.setColData({ isRander: true });
message.success('克隆测试集成功');
};
showNoDelColConfirm = () => {
confirm({
title: '此测试集合为最后一个集合',
content: '温馨提示:建议不要删除'
});
};
caseCopy = async caseId=> {
let that = this;
let caseData = await that.props.fetchCaseData(caseId);
let data = caseData.payload.data.data;
data = JSON.parse(JSON.stringify(data));
data.casename=`${data.casename}_copy`
delete data._id
const res = await axios.post('/api/col/add_case',data);
if (!res.data.errcode) {
message.success('克隆用例成功');
let colId = res.data.data.col_id;
let projectId=res.data.data.project_id;
await this.getList();
this.props.history.push('/project/' + projectId + '/interface/col/' + colId);
this.setState({
visible: false
});
} else {
message.error(res.data.errmsg);
}
};
showDelCaseConfirm = caseId => {
let that = this;
const params = this.props.match.params;
confirm({
title: '您确认删除此测试用例',
content: '温馨提示:用例删除后无法恢复',
okText: '确认',
cancelText: '取消',
async onOk() {
const res = await axios.get('/api/col/del_case?caseid=' + caseId);
if (!res.data.errcode) {
message.success('删除用例成功');
that.getList();
// 如果删除当前选中 case切换路由到集合
if (+caseId === +that.props.currCaseId) {
that.props.history.push('/project/' + params.id + '/interface/col/');
} else {
// that.props.fetchInterfaceColList(that.props.match.params.id);
that.props.setColData({ isRander: true });
}
} else {
message.error(res.data.errmsg);
}
}
});
};
showColModal = (type, col) => {
const editCol =
type === 'edit' ? { colName: col.name, colDesc: col.desc } : { colName: '', colDesc: '' };
this.setState({
colModalVisible: true,
colModalType: type || 'add',
editColId: col && col._id
});
this.form.setFieldsValue(editCol);
};
saveFormRef = form => {
this.form = form;
};
selectInterface = (importInterIds, selectedProject) => {
this.setState({ importInterIds, selectedProject });
};
showImportInterfaceModal = async colId => {
// const projectId = this.props.match.params.id;
// console.log('project', this.props.curProject)
const groupId = this.props.curProject.group_id;
await this.props.fetchProjectList(groupId);
// await this.props.fetchInterfaceListMenu(projectId)
this.setState({ importInterVisible: true, importColId: colId });
};
handleImportOk = async () => {
const project_id = this.state.selectedProject || this.props.match.params.id;
const { importColId, importInterIds } = this.state;
const res = await axios.post('/api/col/add_case_list', {
interface_list: importInterIds,
col_id: importColId,
project_id
});
if (!res.data.errcode) {
this.setState({ importInterVisible: false });
message.success('导入集合成功');
// await this.props.fetchInterfaceColList(project_id);
this.getList();
this.props.setColData({ isRander: true });
} else {
message.error(res.data.errmsg);
}
};
handleImportCancel = () => {
this.setState({ importInterVisible: false });
};
filterCol = e => {
const value = e.target.value;
// console.log('list', this.props.interfaceColList);
// const newList = produce(this.props.interfaceColList, draftList => {})
// console.log('newList',newList);
this.setState({
filterValue: value,
list: JSON.parse(JSON.stringify(this.props.interfaceColList))
// list: newList
});
};
onDrop = async e => {
// const projectId = this.props.match.params.id;
const { interfaceColList } = this.props;
const dropColIndex = e.node.props.pos.split('-')[1];
const dropColId = interfaceColList[dropColIndex]._id;
const id = e.dragNode.props.eventKey;
const dragColIndex = e.dragNode.props.pos.split('-')[1];
const dragColId = interfaceColList[dragColIndex]._id;
const dropPos = e.node.props.pos.split('-');
const dropIndex = Number(dropPos[dropPos.length - 1]);
const dragPos = e.dragNode.props.pos.split('-');
const dragIndex = Number(dragPos[dragPos.length - 1]);
if (id.indexOf('col') === -1) {
if (dropColId === dragColId) {
// 同一个测试集合下的接口交换顺序
let caseList = interfaceColList[dropColIndex].caseList;
let changes = arrayChangeIndex(caseList, dragIndex, dropIndex);
axios.post('/api/col/up_case_index', changes).then();
}
await axios.post('/api/col/up_case', { id: id.split('_')[1], col_id: dropColId });
// this.props.fetchInterfaceColList(projectId);
this.getList();
this.props.setColData({ isRander: true });
} else {
let changes = arrayChangeIndex(interfaceColList, dragIndex, dropIndex);
axios.post('/api/col/up_col_index', changes).then();
this.getList();
}
};
enterItem = id => {
this.setState({ delIcon: id });
};
leaveItem = () => {
this.setState({ delIcon: null });
};
render() {
// const { currColId, currCaseId, isShowCol } = this.props;
const { colModalType, colModalVisible, importInterVisible } = this.state;
const currProjectId = this.props.match.params.id;
// const menu = (col) => {
// return (
// <Menu>
// <Menu.Item>
// <span onClick={() => this.showColModal('edit', col)}>修改集合</span>
// </Menu.Item>
// <Menu.Item>
// <span onClick={() => {
// this.showDelColConfirm(col._id)
// }}>删除集合</span>
// </Menu.Item>
// <Menu.Item>
// <span onClick={() => this.showImportInterface(col._id)}>导入接口</span>
// </Menu.Item>
// </Menu>
// )
// };
const defaultExpandedKeys = () => {
const { router, currCase, interfaceColList } = this.props,
rNull = { expands: [], selects: [] };
if (interfaceColList.length === 0) {
return rNull;
}
if (router) {
if (router.params.action === 'case') {
if (!currCase || !currCase._id) {
return rNull;
}
return {
expands: this.state.expands ? this.state.expands : ['col_' + currCase.col_id],
selects: ['case_' + currCase._id + '']
};
} else {
let col_id = router.params.actionId;
return {
expands: this.state.expands ? this.state.expands : ['col_' + col_id],
selects: ['col_' + col_id]
};
}
} else {
return {
expands: this.state.expands ? this.state.expands : ['col_' + interfaceColList[0]._id],
selects: ['col_' + interfaceColList[0]._id]
};
}
};
const itemInterfaceColCreate = interfaceCase => {
return (
<TreeNode
style={{ width: '100%' }}
key={'case_' + interfaceCase._id}
title={
<div
className="menu-title"
onMouseEnter={() => this.enterItem(interfaceCase._id)}
onMouseLeave={this.leaveItem}
title={interfaceCase.casename}
>
<span className="casename">{interfaceCase.casename}</span>
<div className="btns">
<Tooltip title="删除用例">
<Icon
type="delete"
className="interface-delete-icon"
onClick={e => {
e.stopPropagation();
this.showDelCaseConfirm(interfaceCase._id);
}}
style={{ display: this.state.delIcon == interfaceCase._id ? 'block' : 'none' }}
/>
</Tooltip>
<Tooltip title="克隆用例">
<Icon
type="copy"
className="interface-delete-icon"
onClick={e => {
e.stopPropagation();
this.caseCopy(interfaceCase._id);
}}
style={{ display: this.state.delIcon == interfaceCase._id ? 'block' : 'none' }}
/>
</Tooltip>
</div>
</div>
}
/>
);
};
let currentKes = defaultExpandedKeys();
// console.log('currentKey', currentKes)
let list = this.state.list;
if (this.state.filterValue) {
let arr = [];
list = list.filter(item => {
item.caseList = item.caseList.filter(inter => {
if (inter.casename.indexOf(this.state.filterValue) === -1
&& inter.path.indexOf(this.state.filterValue) === -1
) {
return false;
}
return true;
});
arr.push('col_' + item._id);
return true;
});
// console.log('arr', arr);
if (arr.length > 0) {
currentKes.expands = arr;
}
}
// console.log('list', list);
// console.log('currentKey', currentKes)
return (
<div>
<div className="interface-filter">
<Input placeholder="搜索测试集合" onChange={this.filterCol} />
<Tooltip placement="bottom" title="添加集合">
<Button
type="primary"
style={{ marginLeft: '16px' }}
onClick={() => this.showColModal('add')}
className="btn-filter"
>
添加集合
</Button>
</Tooltip>
</div>
<div className="tree-wrapper" style={{ maxHeight: parseInt(document.body.clientHeight) - headHeight + 'px'}}>
<Tree
className="col-list-tree"
defaultExpandedKeys={currentKes.expands}
defaultSelectedKeys={currentKes.selects}
expandedKeys={currentKes.expands}
selectedKeys={currentKes.selects}
onSelect={this.onSelect}
autoExpandParent
draggable
onExpand={this.onExpand}
onDrop={this.onDrop}
>
{list.map(col => (
<TreeNode
key={'col_' + col._id}
title={
<div className="menu-title">
<span>
<Icon type="folder-open" style={{ marginRight: 5 }} />
<span>{col.name}</span>
</span>
<div className="btns">
<Tooltip title="删除集合">
<Icon
type="delete"
style={{ display: list.length > 1 ? '' : 'none' }}
className="interface-delete-icon"
onClick={() => {
this.showDelColConfirm(col._id);
}}
/>
</Tooltip>
<Tooltip title="编辑集合">
<Icon
type="edit"
className="interface-delete-icon"
onClick={e => {
e.stopPropagation();
this.showColModal('edit', col);
}}
/>
</Tooltip>
<Tooltip title="导入接口">
<Icon
type="plus"
className="interface-delete-icon"
onClick={e => {
e.stopPropagation();
this.showImportInterfaceModal(col._id);
}}
/>
</Tooltip>
<Tooltip title="克隆集合">
<Icon
type="copy"
className="interface-delete-icon"
onClick={e => {
e.stopPropagation();
this.copyInterface(col);
}}
/>
</Tooltip>
</div>
{/*<Dropdown overlay={menu(col)} trigger={['click']} onClick={e => e.stopPropagation()}>
<Icon className="opts-icon" type='ellipsis'/>
</Dropdown>*/}
</div>
}
>
{col.caseList.map(itemInterfaceColCreate)}
</TreeNode>
))}
</Tree>
</div>
<ColModalForm
ref={this.saveFormRef}
type={colModalType}
visible={colModalVisible}
onCancel={() => {
this.setState({ colModalVisible: false });
}}
onCreate={this.addorEditCol}
/>
<Modal
title="导入接口到集合"
visible={importInterVisible}
onOk={this.handleImportOk}
onCancel={this.handleImportCancel}
className="import-case-modal"
width={800}
>
<ImportInterface currProjectId={currProjectId} selectInterface={this.selectInterface} />
</Modal>
</div>
);
}
}

Some files were not shown because too many files have changed in this diff Show More