add new ui base, update readme

pull/40/head
sunfei 5 years ago
parent 2ac6012912
commit 36354ab653

@ -1,31 +1,16 @@
<div align="center"> <div align="center">
<h1>DEV Community 👩‍💻👨‍💻</h1> <h1>I'M DEV(我是开发)👩‍💻👨‍💻</h1>
</div> </div>
Welcome to the [im.dev](https://im.dev) codebase. We are so excited to have you. With your help, we can build out DEV to be more stable and better serve our community.
## What is im.dev? IM DEV旨在打造全球最大的开源开发者社区你可以在这个平台上写文章、在线交流、参与讨论、使用开发学院提升自己的技能、借由平台的个人数据打造自己的个性简历。
[im.dev](https://im.dev) is a platform where software developers write articles, take part in discussions, and build their professional profiles. We value supportive and constructive dialogue in the pursuit of great code and career growth for all members. The ecosystem spans from beginner to advanced developers, and all are welcome to find their place within our community. ❤️ 在招聘板块你和HR可以互相选择因为平台的数据不会说谎。HR在选择你时了更多的判断依据同时你在选择HR时也有了判断依据避免被无良公司HR坑害。
## Contributing 除了分享知识,提升自己,你还能打造自己的个人专业形象,借由这个形象实现金钱上的收入。例如你可以编写收费电子书籍、在开发学院中创作收费课程,还可以出售自己的时间,为他人提供外包、咨询等技术服务。
在这个过程中,平台对你的用户画像会起到重要的作用,会极大的增强其它用户在选择你时的信心。
We encourage you to contribute to im.dev! Please check out the [Contributing Guide](https://github.com/thinkindev/community/blob/master/CONTRIBUTING.md) for guidelines about how to proceed.
## Codebase
### The stack
All of our backend is written in go, and frontend is written in vuejs, our stack goal is performance and easy-deployment.
## License
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Please see the [LICENSE](./LICENSE.md) file in our repository for the full text.
Like many open source projects, we require that contributors provide us with a Contributor License Agreement (CLA). By submitting code to the DEV project, you are granting us a right to use that code under the terms of the CLA.
Our version of the CLA was adapted from the Microsoft Contributor License Agreement, which they generously made available to the public domain under Creative Commons CC0 1.0 Universal.
总之这个开发者社区应有尽有但是又不会像CSDN那样杂乱无章所有的逻辑会通过统一的数据流关联在一起如果你不满意你还能参与到开源中来打造你想要的个性化平台。
<br> <br>

@ -0,0 +1,4 @@
ENV = 'development'
# base api
REACT_APP_ADMIN_BASE_API = '/dev-api'

@ -0,0 +1,4 @@
ENV = 'production'
# base api
REACT_APP_ADMIN_BASE_API = '/pro-api'

@ -0,0 +1,70 @@
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly",
"require": true,
"module": true,
"__dirname": true,
"process": true
},
// 解析器用于解析代码
"parser": "babel-eslint",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react",
"react-hooks"
],
"rules": {
// no-var
'no-var': 'error',
// 要求或禁止 var 声明中的初始化
'init-declarations': 2,
// 强制使用单引号
'quotes': ['error', 'single'],
// 要求或禁止使用分号而不是 ASI
'semi': ['error', 'never'],
// 禁止不必要的分号
'no-extra-semi': 'error',
// 强制使用一致的换行风格
'linebreak-style': ['error', 'unix'],
// 空格4个
'indent': ['error', 4, {'SwitchCase': 1}],
// 指定数组的元素之间要以空格隔开(,后面) never参数[ 之前和 ] 之后不能带空格always参数[ 之前和 ] 之后必须带空格
'array-bracket-spacing': [2, 'never'],
// 在块级作用域外访问块内定义的变量是否报错提示
'block-scoped-var': 0,
// if while function 后面的{必须与if在同一行java风格。
'brace-style': [2, '1tbs', {'allowSingleLine': true}],
// 双峰驼命名格式
'camelcase': 2,
// 数组和对象键值对最后一个逗号, never参数不能带末尾的逗号, always参数必须带末尾的逗号
'comma-dangle': [2, 'never'],
// 控制逗号前后的空格
'comma-spacing': [2, {'before': false, 'after': true}],
// 控制逗号在行尾出现还是在行首出现
'comma-style': [2, 'last'],
// 圈复杂度
'complexity': [2, 9],
// 以方括号取对象属性时,[ 后面和 ] 前面是否需要空格, 可选参数 never, always
'computed-property-spacing': [2, 'never'],
// TODO 关闭 强制方法必须返回值TypeScript强类型不配置
// 'consistent-return': 0
// react-hooks
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
};

29
ui/.gitignore vendored

@ -1,14 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store .DS_Store
node_modules/ .env.local
/dist/ .env.development.local
.env.test.local
.env.production.local
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln

@ -1,21 +1,15 @@
# vue-starter ## 可用的脚本
> A vuejs starter project integrated with vuex, vue-router, less, iview 在项目目录下,你可以运行:
## Build Setup ### `yarn run sm`
``` bash 在开发模式下运行app.
# install dependencies
npm install
# serve with hot reload at localhost:8080 打开[http://localhost:3000](http://localhost:3000)在浏览器中查看。
npm run dev
# build for production with minification
npm run build
# build for production and view the bundle analyzer report 在开发模式下运行JSON Server服务.
npm run build --report
``` 打开[http://localhost:3023](http://localhost:3023)在浏览器中查看。
For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).

@ -0,0 +1,60 @@
const {
override,
addDecoratorsLegacy,
fixBabelImports,
addLessLoader,
addWebpackAlias
// addWebpackPlugin
} = require('customize-cra')
const themeVariables = require('./src/styles/theme/themeVariables')
const AntDesignThemePlugin = require('antd-theme-webpack-plugin')
const path = require('path')
const options = {
antDir: path.join(__dirname, './node_modules/antd'), // antd包位置
stylesDir: path.join(__dirname, './src/styles'), //主题文件所在文件夹
varFile: path.join(__dirname, './src/styles/vars.less'), // 自定义默认的主题色
mainLessFile: path.join(__dirname, './src/styles/main.less'), // 项目中其他自定义的样式(如果不需要动态修改其他样式,该文件可以为空)
outputFilePath: path.join(__dirname, './public/color.less'), //提取的less文件输出到什么地方
themeVariables: themeVariables, //要改变的主题变量
indexFileName: './public/index.html', // index.html所在位置
generateOnce: false // 是否只生成一次
}
const addTheme = () => (config) => {
config.plugins.push(new AntDesignThemePlugin(options))
return config
}
module.exports = override(
// 使用 Day.js 替换 momentjs 优化打包大小
// addWebpackPlugin(
// ),
// 装饰器语法
addDecoratorsLegacy(),
// 自动加载antd
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true
}),
// less
addLessLoader({
javascriptEnabled: true,
localIdentName: '[local]--[hash:base64:5]'
}),
// 路径别名
addWebpackAlias({
'@': path.resolve(__dirname, 'src'),
'@library': path.resolve(__dirname, 'src/library'),
'@layouts': path.resolve(__dirname, 'src/layouts'),
'@utils': path.resolve(__dirname, 'src/library/utils'),
'@pages': path.resolve(__dirname, 'src/pages'),
'@store': path.resolve(__dirname, 'src/store'),
'@styles': path.resolve(__dirname, 'src/styles'),
'@components': path.resolve(__dirname, 'src/components'),
'@http': path.resolve(__dirname, 'src/library/utils/http')
}),
// 主题
addTheme()
)

@ -0,0 +1,31 @@
[react 文档](https://zh-hans.reactjs.org/)
[Eslint 参考资料](https://blog.csdn.net/walid1992/article/details/54633760)
[mock 文档](http://mockjs.com/)
[json-server 参考资料](https://blog.csdn.net/div_ma/article/details/80579971)
[mockjs 参考资料](https://blog.csdn.net/div_ma/article/details/80592996)
[mock json-server 虚拟数据服务搭建](https://blog.csdn.net/qq_41629150/article/details/99645632)
## 安装依赖
```javascript
npm install json-server -g
yarn global add json-server
```
## 使用 concurrently 并行地运行多个命令
```javascript
npm i concurrently --save
yarn add concurrently
```

@ -0,0 +1,6 @@
module.exports = {
HOST: 'localhost', // 定义ip地址
PORT: '3023', // 定义端口号
DB_FILE: './db.js', // 定义批量模拟数据文件
API: '/api'//创建根api名 这里的 /mock 如同 后端真实/api
}

@ -0,0 +1,25 @@
let Mock = require('mockjs')
let Random = Mock.Random
module.exports = function() {
let data = {
news: []
}
let images = [1, 2, 3].map( x =>Random.image('200x100', Random.color(), Random.word(2, 6)))
for (let i = 0; i < 100; i++) {
let content = Random.cparagraph(0, 10)
data.news.push({
id: i,
title: Random.cword(8, 20),
desc: content.substr(0, 40),
tag: Random.cword(2, 6),
views: Random.integer(100, 5000),
images: images.slice(0, Random.integer(1, 3))
})
}
return data
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

@ -0,0 +1,85 @@
<html>
<head>
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay"
crossorigin="anonymous"
/>
<title>JSON Server</title>
<link rel="shortcut icon" href="favicon.ico"><link href="main.css" rel="stylesheet"></head>
<body>
<header>
<div class="container">
<nav>
<ul>
<li class="title">
JSON Server
</li>
<li>
<a href="https://github.com/users/typicode/sponsorship">
<i class="fas fa-heart"></i>GitHub赞助商
</a>
</li>
<li>
<a href="https://my-json-server.typicode.com">
<i class="fas fa-burn"></i>远程
</a>
</li>
<li>
<a href="https://thanks.typicode.com">
<i class="far fa-laugh"></i>支持者
</a>
</li>
</ul>
</nav>
</div>
</header>
<main>
<div class="container">
<h1>恭喜!</h1>
<p>
您成功运行了JSON服务器
<br />
✧*。٩(ˊᗜˋ*)و✧*。
</p>
<div id="resources"></div>
<p>
要访问和修改资源您可以使用HTTP方法:
</p>
<p>
<code>GET[获取资源]</code>
<code>POST[新建资源]</code>
<code>PUT[全量更新资源]</code>
<code>PATCH[局部更新资源]</code>
<code>DELETE[删除资源]</code>
<code>OPTIONS</code>
</p>
<p>
<a href="http://www.ruanyifeng.com/blog/2014/05/restful_api.html" target="_blank">RESTful API 设计指南</a>
</p>
<div id="custom-routes"></div>
<h1>文档</h1>
<p>
<a href="https://github.com/typicode/json-server">
README
</a>
</p>
</div>
</main>
<footer>
<div class="container">
<p>
若要替换此页,请创建
<code>./public/index.html</code> 文件.
</p>
<p>此页面为已创建覆盖页面</p>
</div>
</footer>
<script type="text/javascript" src="main.js"></script></body>
</html>

@ -0,0 +1,114 @@
body {
display: flex;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
flex-direction: column;
padding: 0;
margin: 0;
color: #3b4252;
letter-spacing: 0;
}
.container {
max-width: 960px;
margin: auto;
padding: 1rem;
}
header {
border-bottom: 1px solid #eee;
}
header a {
color: inherit;
text-decoration: none;
}
header a:hover {
text-decoration: underline;
}
nav ul {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
}
nav li.title {
flex-grow: 5;
text-align: left;
font-weight: bold;
font-size: 1.4rem;
color: #3b4252;
}
nav li {
flex-grow: 1;
align-self: center;
text-align: right;
color: #4c566a;
}
.fa-heart {
color: deeppink;
}
main {
flex: 1;
}
footer {
margin-top: 4rem;
border-top: 1px solid #eee;
}
h1 {
margin-top: 4rem;
font-weight: normal;
}
i {
margin-right: 0.5rem;
}
a {
color: #5e81ac;
}
a:hover {
color: #81a1c1;
text-decoration: underline;
}
table {
margin-left: 0;
}
td {
border: 0;
padding: 0 1em 0.5em 0;
}
td:first-child {
width: 1%;
white-space: nowrap;
}
ul {
list-style-position: inside;
padding-left: 0;
}
li {
list-style-type: none;
margin-bottom: 0.2rem;
}
code {
padding: 0.2rem;
margin: 0rem 0.2rem;
border-radius: 0.2rem;
background: #e5e9f0;
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,4 @@
const API = '/api'
module.exports = {
[`${API}` + '/*'] : '/$1' // 路由请求由/mock/*接管
}

@ -0,0 +1,87 @@
const jsonServer = require('json-server')
let Mock = require('mockjs')
let Random = Mock.Random
const server = jsonServer.create()
const middlewares = jsonServer.defaults()
const rules = require('./routes')
const {HOST, PORT, DB_FILE, API} = require('./config.js')
const DB = require(DB_FILE)
const router = jsonServer.router(DB()) // 将所创建的数据对象传入,以之生成相应的路由
server.use(jsonServer.bodyParser)
server.use(middlewares)
server.post(`${API}/reg`, ({body:{username='', password=''}}, res) => {
(username !== 'admin' && password) ?
res.jsonp({
'err': 0,
'msg': '注册成功',
'data': {
username,
password
}
}) :
res.jsonp({
'err': 1,
'msg': '注册失败'
})
})
// 响应/mock/login,进行登录验证操作
server.post(`${API}/login`, ({body:{username='', password=''}}, res) => {
console.log(username),
(username === 'admin' && password === '123456') ?
setTimeout(() => { // 由于本地请求速度较快不方便loading动效显示利用延时器模拟真实服务器请求速度
res.jsonp({
'err': 0,
'msg': '登录成功',
'data': {
'token': '123',
'address': '打破',
'email': 'louis.lyr@outlook.com',
'tel': '15185724613',
'avatar': '' // Random.image('200x200', Random.color(), Random.word(2, 6))
}
})
}, 2000) :
setTimeout(() => { // 由于本地请求速度较快不方便loading动效显示利用延时器模拟真实服务器请求速度
res.jsonp({
'err': 1,
'msg': '登录失败'
})
}, 2000)
})
// 自定义返回内容
router.render = (req, res) => {
let status = ''
let len = Object.keys(res.locals.data).length // 判断是否获取到mockJS模拟的数据
if (res.req.originalMethod === 'DELETE') {
status = len === 0
} else {
status = !!len
}
setTimeout(() => { // 由于本地请求速度较快不方便loading动效显示利用延时器模拟真实服务器请求速度
res.jsonp({ // 使用res.jsonp()方法将mockJS模拟生成的数据进行自定义包装后输出
err: status ? 0 : 1,
msg: '操作' + (status ? '成功' : '失败'),
data: res.locals.data
})
}, 2000)
}
// router.render = (req, res) => {
// console.log(req)
// res.status(500).jsonp({
// error: 'error message here'
// })
// }
server.use(jsonServer.rewriter(rules)) // 根据需要重写路由匹配规则
server.use(router) // 安装路由
server.listen({
host: HOST,
port: PORT
}, function() {
console.log(`JSON Server is running in http://${HOST}:${PORT}`)
})

@ -1,72 +1,71 @@
{ {
"name": "mafanr", "name": "imdev-ui",
"version": "1.0.0", "version": "0.1.0",
"description": "A Vue.js project",
"author": "sunface <cto@188.com>",
"private": true, "private": true,
"dependencies": {
"@ant-design/dark-theme": "^0.2.2",
"@antv/data-set": "^0.10.2",
"@antv/g2plot": "^0.11.5",
"@types/jest": "^24.0.25",
"@types/node": "^13.1.6",
"@types/react": "^16.9.17",
"@types/react-dom": "^16.9.4",
"@types/react-responsive": "^8.0.2",
"@types/react-router-dom": "^5.1.2",
"antd": "3.26.6",
"antd-dayjs-webpack-plugin": "^0.0.7",
"axios": "^0.19.0",
"babel-plugin-import": "^1.12.2",
"bizcharts": "^3.5.6",
"customize-cra": "^0.9.1",
"js-cookie": "^2.2.1",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"mobx": "^5.15.0",
"mobx-react": "^6.1.4",
"react": "^16.12.0",
"react-app-rewired": "^2.1.5",
"react-color": "^2.17.3",
"react-dom": "^16.12.0",
"react-intl": "^3.9.3",
"react-loadable": "^5.5.0",
"react-responsive": "^8.0.1",
"react-router-dom": "^5.1.2",
"react-scripts": "3.2.0",
"typescript": "^3.7.4"
},
"scripts": { "scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "react-app-rewired start",
"start": "npm run dev", "build": "react-app-rewired build",
"build": "node build/build.js" "test": "react-app-rewired test",
"sm": "concurrently \"yarn start\" \"yarn run mock\" ",
"mock": "cd mocks && node ./server.js"
}, },
"dependencies": { "eslintConfig": {
"axios": "0.17.1", "extends": "react-app"
"element-ui": "^2.11.1",
"js-cookie": "2.2.0",
"mavon-editor": "^2.7.5",
"vue": "^2.6.10",
"vue-i18n": "^8.11.2",
"vue-router": "^3.0.3",
"vuex": "^3.1.1"
}, },
"devDependencies": { "browserslist": {
"autoprefixer": "^7.1.2", "production": [
"babel-core": "^6.22.1", ">0.2%",
"babel-helper-vue-jsx-merge-props": "^2.0.3", "not dead",
"babel-loader": "^7.1.1", "not op_mini all"
"babel-plugin-syntax-jsx": "^6.18.0", ],
"babel-plugin-transform-runtime": "^6.22.0", "development": [
"babel-plugin-transform-vue-jsx": "^3.5.0", "last 1 chrome version",
"babel-preset-env": "^1.3.2", "last 1 firefox version",
"babel-preset-stage-2": "^6.22.0", "last 1 safari version"
"chalk": "^2.0.1", ]
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.23.1",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-loader": "^0.3.0",
"html-webpack-plugin": "^2.30.1",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"style-loader": "^0.13.1",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.6.10",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
}, },
"engines": { "devDependencies": {
"node": ">= 6.0.0", "@babel/plugin-proposal-decorators": "^7.7.4",
"npm": ">= 3.0.0" "antd-theme-webpack-plugin": "^1.3.0",
"babel-eslint": "^10.0.3",
"concurrently": "^5.0.0",
"eslint": "^6.7.2",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^2.0.1",
"json-server": "^0.15.1",
"mockjs": "^1.0.1-beta3"
}, },
"browserslist": [ "proxy": "http://localhost:3023"
"> 1%",
"last 2 versions",
"not ie <= 8"
]
} }

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Web site created using create-react-app"/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="apple-touch-icon" href="logo192.png" />
<title>React App</title>
<script>
window.less = {
async: false,
env: 'development'//production development
};
</script>
</head>
<body>
<link rel="stylesheet/less" type="text/css" href="/color.less" />
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="text/javascript" src="/less.min.js"></script>
<!-- <script>
var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串
var isOpera = userAgent.indexOf("Opera") > -1;
console.log(userAgent)
//判断是否Opera浏览器
if (isOpera) {
console.log("Opera");
};
//判断是否Firefox浏览器
if (userAgent.indexOf("Firefox") > -1) {
console.log("FF");
}
//判断是否Chrome浏览器
if (userAgent.indexOf("Chrome") > -1){
console.log("Chrome");
}
//判断是否Safari浏览器
if (userAgent.indexOf("Safari") > -1) {
console.log("Safari");
}
//判断是否IE浏览器
if (!!window.ActiveXObject || "ActiveXObject" in window) {
console.log("IE");
};
</script> -->
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

@ -0,0 +1,21 @@
import React from 'react'
import { ConfigProvider } from 'antd'
import { inject, observer } from 'mobx-react'
import zhCN from 'antd/es/locale/zh_CN'
import enGB from 'antd/es/locale/en_GB'
const Config = inject('system')(observer((props) =>{
let {system} = props
let antdLocale = {}
antdLocale['zh_CN'] = zhCN
antdLocale['en_GB'] = enGB
return (
<>
<ConfigProvider locale={antdLocale[system.locale]}>
{props.children}
</ConfigProvider>
</>
)
}))
export default Config

@ -0,0 +1,19 @@
import React from 'react'
import { inject, observer } from 'mobx-react'
import { IntlProvider } from 'react-intl' /* react-intl imports */
import locale from '@library/locale'
const Intl = inject('system')(observer((props) =>{
let {system} = props
let messages = locale
return (
<>
<IntlProvider locale={system.locale.split('_')[0]} messages={messages[system.locale.split('_')[0]]}>
{props.children}
</IntlProvider>
</>
)
}))
export default Intl

@ -0,0 +1,31 @@
import React from 'react'
import { Icon } from 'antd'
import { inject, observer } from 'mobx-react'
import style from './index.module.less'
const Languages = inject('system')(observer((props) =>{
let {system} = props
const IconFont = Icon.createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_1585712_tvew52du1cn.js'
})
function setLocale(locale){
system.setLocale(locale)
}
return (
<>
<div className={style.languages}>
<div onClick={()=>{setLocale('en_GB')}}>
<IconFont type="icon-yingguo" className={`${style.icon}` } />
<div className={`${system.locale === 'en_GB'?style.selected:''}`}></div>
</div>
<div onClick={()=>{setLocale('zh_CN')}}>
<IconFont type="icon-china" className={`${style.icon}`} />
<div className={`${system.locale === 'zh_CN'?style.selected:''}`}></div>
</div>
</div>
</>
)
}))
export default Languages

@ -0,0 +1,33 @@
@import '../../styles/main.less';
.languages{
display: flex;
&>div{
position: relative;
cursor: pointer;
box-shadow: 0px 0px 4px 1px rgba(220, 223, 225, 0.5);
width: 28px;
height: 28px;
margin-right: 14px;
border-radius: 50%;
transition: all .2s;
}
&>div:hover{
transform: scale(1.3);
}
&>div:last-child{
margin-right: 0;
}
.selected{
position: absolute;
top: 6%;
left: 6%;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #fff;
}
}
.icon{
font-size: 28px;
}

@ -0,0 +1,17 @@
import React from 'react'
import ReactDOM from 'react-dom'
import * as serviceWorker from './serviceWorker'
import App from './pages/App'
import { BrowserRouter as Router} from 'react-router-dom'
import { Provider } from 'mobx-react'
import stores from './store'
ReactDOM.render(
<Router>
<Provider {...stores}>
<App />
</Provider>
</Router>
, document.getElementById('root'))
serviceWorker.unregister()

@ -0,0 +1,33 @@
import React from 'react'
import { useLocation } from 'react-router-dom'
import { Breadcrumb } from 'antd'
import style from './index.module.less'
import { inject, observer } from 'mobx-react'
import { FormattedMessage as Message } from 'react-intl'
const BreadcrumbWrapper = inject('system')(observer((props) =>{
let {system} = props
let location = useLocation()
let pathname = location.pathname.split('/')
return (
<>
<div className={`${style.breadcrumb}`}>
<div>{<Message id={pathname[pathname.length-1]}/>}</div>
<Breadcrumb separator=">">
{
pathname.map((item, key)=>{
if(item.length > 0){
return (
<Breadcrumb.Item key={key}><Message id={item}/></Breadcrumb.Item>
)
}
return ''
})
}
</Breadcrumb>
</div>
</>
)
}))
export default BreadcrumbWrapper

@ -0,0 +1,7 @@
@import '../../styles/main.less';
.breadcrumb{
display: flex;
justify-content: space-between;
padding: 6px 20px;
}

@ -0,0 +1,53 @@
import React, { Suspense, useState, useEffect } from 'react'
import { Layout, BackTop } from 'antd'
import BreadcrumbWrapper from '@layouts/Breadcrumb'
import { Route } from 'react-router-dom'
import { isEmpty } from '@library/utils/validate'
const { Content } = Layout
// import {
// TransitionGroup,
// CSSTransition
// } from "react-transition-group";
function ContentWrapper(porps){
let { routers } = porps
let [routeItem, setRouteItem] = useState([])
useEffect( () => {
let item = []
routers.map((route) => {
if(!isEmpty(route.children)){
route.children.map((r) => {
item.push(r)
return ''
})
}else{
item.push(route)
}
return ''
})
setRouteItem(item)
return ()=>{
setRouteItem([])
}
}, [routers])
return(
<>
<Content>
<Suspense fallback={<div></div>}>
<BreadcrumbWrapper />
{
routeItem.map((route, key) => {
return(
<Route key={`${key}`} path={route.path} component={route.component}/>
)
})
}
{/* <Route path="/home/*" component={Error404} /> */}
</Suspense>
<BackTop />
</Content>
</>
)
}
export default ContentWrapper

@ -0,0 +1,56 @@
import React from 'react'
import { Drawer, Row, Switch } from 'antd'
import { inject, observer } from 'mobx-react'
import style from './index.module.less'
import { CirclePicker } from 'react-color'
import { modifyVars } from '@library/utils/modifyVars'
import Languages from '@components/Languages'
import { FormattedMessage as Message } from 'react-intl'
const DrawerWrapper = inject('system')(observer((props) =>{
let {system} = props
let primary = (color)=>{
system.setPrimary(color.hex)
modifyVars(system.dark, color.hex)
// modifyVars({'@primary-color': color.hex})
}
let dark = ()=>{
system.setDark()
modifyVars(system.dark, system.primary)
}
return (
<>
<div>
<Drawer
title={<Message id='setting'/>}
width={380}
onClose={system.setDrawer}
visible={system.drawer}
bodyStyle={{ paddingBottom: 80 }}
className={`${style.drawer} ${system.dark?style.dark:''}`}
>
<Row>
<div className={style.row}>
<div><Message id='model'/></div>
<div><Switch defaultChecked onClick={ dark } /></div>
</div>
<div className={style.row}>
<div><Message id='themes'/></div>
<div>
<CirclePicker color={system.primary} onChangeComplete={ primary }/>
</div>
</div>
<div className={style.row}>
<div><Message id='languages'/></div>
<div>
<Languages />
</div>
</div>
</Row>
</Drawer>
</div>
</>
)
}))
export default DrawerWrapper

@ -0,0 +1,28 @@
@import '../../styles/main.less';
.drawer{
:global {
.ant-col{
margin-bottom: 20px;
}
}
.row {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
}
.column {
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.title {
font-size: 16px;
color: #3b4859;
margin-bottom: 20px;
}
.sub_title {
font-size: 14px;
color: #8998ac;
}
}

@ -0,0 +1,87 @@
import React from 'react'
import { Layout, Icon, Badge, Avatar, Popover } from 'antd'
import { useHistory } from 'react-router-dom'
import { inject, observer } from 'mobx-react'
import { useMediaQuery } from 'react-responsive'
import { removeToken } from '@utils/auth'
import style from './index.module.less'
const { Header } = Layout
const HeaderWrapper = inject('system', 'user')(observer((props) =>{
let history = useHistory()
let {system, user} = props
const isMobile = useMediaQuery({
query: '(max-device-width: 991px)'
})
const onClickLogout = ()=>{
removeToken()
history.push('/login')
}
const userPopover = (
<div className={style.userPopover}>
<div onClick={onClickLogout}><Icon type="logout" /><span>退出</span></div>
</div>
)
const messagePopover = (
<div className={style.messagePopover}>
<div>ss</div>
</div>
)
return (
<>
<Header>
{
isMobile ?
<>dsa</>
:
<div className={style.header}>
<div>
<Icon type={system.collapsed?'menu-unfold':'menu-fold'} className={style.menu_icon} onClick={()=>{system.setCollapsed()}} />
</div>
<div>
<div>
{/* <AutoComplete
className="certain-category-search"
dropdownClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth={false}
dropdownStyle={{ width: 300 }}
size="large"
style={{ width: '100%' }}
placeholder="input here"
optionLabelProp="value"
>
<Input suffix={<Icon type="search" className={`${style.icon}`} />} />
</AutoComplete> */}
</div>
<div>
<Badge dot>
<Popover placement="bottomRight" content={messagePopover}>
<Icon type="bell" className={style.icon} />
</Popover>
</Badge>
</div>
<div>
<Popover className={`${style.pointer}`} placement="bottomRight" content={userPopover}>
<Badge dot>
<Avatar icon="user" src={user.info.get('avatar')}/>
</Badge>
</Popover>
</div>
<div>
<Icon type="align-left" className={style.icon} onClick={()=>{system.setDrawer()}} />
</div>
</div>
</div>
}
</Header>
</>
)
}))
export default HeaderWrapper

@ -0,0 +1,56 @@
@import '../../styles/main.less';
.header{
height: 100%;
display: flex;
justify-content: space-between;
box-sizing: border-box;
&>div{
display: flex;
align-items: center;
&>div{
display: flex;
align-items: center;
justify-content: center;
height: 64px;
width: 50px;
}
}
.menu_icon{
display: block;
font-size: 18px;
}
.icon{
display: block;
font-size: 18px;
}
.plummer{
position: relative;
}
}
.pointer{
cursor: pointer;
}
.messagePopover{
width: 400px;
}
.userPopover{
width: 200px;
display: flex;
flex-direction: column;
&>div{
width: 100%;
height: 48px;
line-height: 48px;
cursor: pointer;
& > i{
margin-right: 6px;
}
&:hover{
&>span, &>i{
color: @dropdown-selected-color;
}
}
}
}

@ -0,0 +1,21 @@
import React from 'react'
import { Icon } from 'antd'
import style from './index.module.less'
import { inject, observer } from 'mobx-react'
let Logo = inject('system')(observer((props) => {
let {system} = props
return(
<div className={style.logo}>
{/* ${display?style.title:style.title_hidden} */}
<div>
dsa
</div>
<div onClick={()=>{system.setCollapsed()}} className={`${style.icon} ${system.collapsed?style.icon_transition:''}`}>
<Icon type="menu-unfold" />
</div>
</div>
)
}))
export default Logo

@ -0,0 +1,33 @@
.logo{
width: 100%;
display: flex;
font-size: 18px;
&>div{
display: flex;
align-items: center;
}
.title{
flex: 1;
font-weight:900;
color: #3B4859;
box-sizing: border-box;
padding-left: 19px;
&>span:first-of-type{
color: #3B7CFF;
}
}
.title_hidden{
display: none;
}
.icon{
height: 64px;
width:64px;
justify-content: center;
transform: rotate(180deg);
cursor: pointer;
}
.icon_transition{
transform: rotate(0);
}
}

@ -0,0 +1,70 @@
import React from 'react'
import { useLocation } from 'react-router-dom'
import { Menu, Icon } from 'antd'
import { Link } from 'react-router-dom'
import { inject, observer } from 'mobx-react'
import { isEmpty } from '@library/utils/validate'
import style from './index.module.less'
import { FormattedMessage as Message } from 'react-intl'
const { SubMenu } = Menu
const MenuWrapper = inject('system')(observer((props) =>{
let {system, routers} = props
let location = useLocation()
return (
<>
<div className={style.menu}>
<div className={style.logo}>
</div>
<Menu
mode={`${system.collapsed ? 'vertical' : 'inline'}`}
theme={`${system.theme}`}
selectedKeys={[location.pathname]}
forceSubMenuRender={true}
>
{
routers.map((route) => {
if(isEmpty(route.children)){
return (
<Menu.Item key={`${route.path}`}>
<Link to={route.path}>
<Icon type={route.icon}/>
<span><Message id={route.title}/></span>
</Link>
</Menu.Item>
)
}else{
let items = []
route.children.map((r) => {
items.push(
<Menu.Item key={r.path}>
<Link to={r.path}>
{
isEmpty(r.icon)?'':<Icon type={r.icon}/>
}
<span><Message id={r.title}/></span>
</Link>
</Menu.Item>
)
return ''
})
return (
<SubMenu key={`${route.path}`} title={
<span>
<Icon type={route.icon} />
<span><Message id={route.title}/></span>
</span>
}>
{items}
</SubMenu>
)
}
})
}
</Menu>
</div>
</>
)
}))
export default MenuWrapper

@ -0,0 +1,7 @@
.menu{
.logo{
height: 32px;
background: #aaa;
margin: 16px;
}
}

@ -0,0 +1,4 @@
import Layout from '@/layouts/Layout'
export {
Layout
}

@ -0,0 +1,16 @@
const enUS = {
home: 'Home',
dashboard: 'Dashboard',
chart: 'Chart',
charts: 'Charts',
element: 'Element',
icons: 'Icons',
accordion: 'Accordion',
paginations: 'Paginations',
datePickers: 'DatePickers',
setting: 'Setting',
themes: 'Themes',
model: 'Model',
languages: 'Languages'
}
export default enUS

@ -0,0 +1,8 @@
import zhCN from '@library/locale/zh_CN' // 中文
import enUS from '@library/locale/en_US' // 英文
export default {
'en': enUS,
'zh': zhCN
}

@ -0,0 +1,16 @@
const zhCN = {
home: '主页',
dashboard: '仪表盘',
chart: '图表',
charts: '图表',
element: '元素',
icons: '图标',
accordion: '手风琴',
paginations: '页码',
datePickers: '时间',
setting: '设置',
themes: '主题',
model: '模式',
languages: '语言'
}
export default zhCN

@ -0,0 +1,28 @@
import React from 'react'
import Element from '@library/routes/modules/Element'
const Dashboard = React.lazy(() => import('@pages/Index/Dashboard'))
const Charts = React.lazy(() => import('@pages/Index/Charts'))
const Routers = [
{
path: '/home/dashboard',
title: 'dashboard',
icon: 'dashboard',
component: Dashboard
},
{
path: '/home/charts',
title: 'chart',
icon: 'pie-chart',
component: Charts
},
{
path: '/home/element',
title: 'element',
icon: 'build',
children: Element
}
]
export default Routers

@ -0,0 +1,19 @@
import React from 'react'
const A = React.lazy(() => import('@pages/Index/Demo/A'))
const B = React.lazy(() => import('@pages/Index/Demo/B'))
const Demo = [
{
path: '/a',
title: 'A',
icon: 'bar-chart',
component: A
},
{
path: '/b',
title: 'B',
icon: 'bar-chart',
component: B
}
]
export default Demo

@ -0,0 +1,34 @@
import React from 'react'
const Accordion = React.lazy(() => import('@pages/Index/Elements/Accordion'))
const Paginations = React.lazy(() => import('@pages/Index/Elements/Paginations'))
const DatePickers = React.lazy(() => import('@pages/Index/Elements/DatePickers'))
const Icons = React.lazy(() => import('@pages/Index/Elements/Icons'))
const Demo = [
{
path: '/home/element/accordion',
title: 'accordion',
icon: 'pic-center',
component: Accordion
},
{
path: '/home/element/paginations',
title: 'paginations',
icon: 'pic-center',
component: Paginations
},
{
path: '/home/element/datePickers',
title: 'datePickers',
icon: 'calendar',
component: DatePickers
},
{
path: '/home/element/icons',
title: 'icons',
icon: 'smile',
component: Icons
}
]
export default Demo

@ -0,0 +1,15 @@
import Cookies from 'js-cookie'
const TokenKey = 'admin-token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}

@ -0,0 +1,29 @@
import axios from 'axios'
if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'https://www.baidu.com'
}else{
axios.defaults.baseURL = 'https://www.production.com'
}
let loadingInstance = null //这里是loading
//使用create方法创建axios实例
export const Service = axios.create({
timeout: 7000, // 请求超时时间
method: 'post',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 添加请求拦截器
Service.interceptors.request.use(config => {
return config
})
// 添加响应拦截器
Service.interceptors.response.use(response => {
loadingInstance.close()
// console.log(response)
return response.data
}, error => {
return Promise.reject(error)
})

@ -0,0 +1,152 @@
import axios from 'axios'
import {message} from 'antd'
import QS from 'qs'
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。'
}
// 全局的默认值
if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'https://www.baidu.com'
}else{
axios.defaults.baseURL = 'http://localhost:3023'
//axios.defaults.baseURL = 'http://rap2api.taobao.org/app/mock/242047'
}
// 请求超时时间
axios.defaults.timeout = 10000
// post请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
// 请求拦截器
axios.interceptors.request.use(
config => {
// 每次发送请求之前判断是否存在token如果存在则统一在http请求的header都加上token不用每次请求都手动添加了
// 即使本地存在token也有可能token是过期的所以在响应拦截器中要对返回状态进行判断
const token = '1'
token && (config.headers.Authorization = token)
return config
},
error => {
return error
}
)
// 响应拦截器
axios.interceptors.response.use(
response => {
if (response.status === 200) {
return response
} else {
return response
}
},
// 服务器状态码不是200的情况
error => {
if (error.response.status) {
switch (error.response.status) {
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401:
message.error(`${codeMessage[error.response.status]}`)
// router.replace({
// path: '/login',
// query: { redirect: router.currentRoute.fullPath }
// });
break
// 403 token过期
// 登录过期对用户进行提示
// 清除本地token和清空vuex中token对象
// 跳转登录页面
case 403:
message.error(`${codeMessage[error.response.status]}`)
// 清除token
// localStorage.removeItem('token');
// store.commit('loginSuccess', null);
// // 跳转登录页面并将要浏览的页面fullPath传过去登录成功后跳转需要访问的页面
// setTimeout(() => {
// router.replace({
// path: '/login',
// query: {
// redirect: router.currentRoute.fullPath
// }
// });
// }, 1000);
break
// 404请求不存在
case 404:
message.error(`${codeMessage[error.response.status]}`)
// Toast({
// message: '网络请求不存在',
// duration: 1500,
// forbidClick: true
// });
break
// 其他错误,直接抛出错误提示
default:
message.warning(`${codeMessage[error.response.status]}`)
// Toast({
// message: error.response.data.message,
// duration: 1500,
// forbidClick: true
// });
}
return error.response
}
}
)
/**
* get方法对应get请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
* @param {Function} successBack [请求时成功回调方法]
* @param {Function} errorBack [请求时失败回调方法]
* @param {Function} finallyBack [无论有无异常都会执行回调方法]
*/
export async function get(url, params, successBack = function(){}, errorBack = function(){}, finallyBack = function(){}){
try {
const res = await axios.get(url, params)
successBack(res.data)
} catch(err) {
errorBack(err)
} finally {
finallyBack()
}
}
/**
* post方法对应post请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
* @param {Function} successBack [请求时成功回调方法]
* @param {Function} errorBack [请求时失败回调方法]
* @param {Function} finallyBack [无论有无异常都会执行回调方法]
*/
export async function post(url, params, successBack = function(){}, errorBack = function(){}, finallyBack = function(){}) {
try {
const res = await axios.post(url, QS.stringify(params))
successBack(res.data)
} catch(err) {
errorBack(err)
} finally {
finallyBack()
}
}

@ -0,0 +1,13 @@
let adminKey = 'admin-'
const storage = {
set(key, value){
localStorage.setItem(adminKey+key, JSON.stringify(value))
},
get(key){
return JSON.parse(localStorage.getItem(adminKey+key))
},
remove(key){
localStorage.removeItem(adminKey+key)
}
}
export default storage

@ -0,0 +1,18 @@
import {Light, Dark} from '@styles/theme'
export function modifyVars(model, primary){
//window.less.modifyVars(vars)
if(model){
window.less.modifyVars(Dark(primary))
}else{
window.less.modifyVars(Light(primary))
}
}
// export function modifyModel(model){
// // let {dark, light} = theme
// // let dark = Dark
// // console.log(Dark())
// // console.log(Light())
// }

@ -0,0 +1,16 @@
/**
* 判断是否为空
* @param {*} value
* @returns {Boolean}
*/
export function isEmpty(value){
if(value === null || value === '' || value === 'undefined' || value === undefined || value === 'null' || value.length === 0){
return true
} else{
// value = value.replace(/\s/g, '')
// if(value === ''){
// return true
// }
return false
}
}

@ -0,0 +1,38 @@
import React, { useEffect } from 'react'
import {Route, Switch, Redirect} from 'react-router-dom'
import { modifyVars } from '../../library/utils/modifyVars'
import { inject, observer } from 'mobx-react'
import '@/styles/main.less'
import Home from '../../pages/Index'
import Login from '../../pages/Login'
import ConfigProvider from '../../components/ConfigProvider'
import Intl from '../../components/Intl'
let App = inject('system')(observer((props:any) => {
let {system} = props
//npm install --save rc-form-hooks
// https://www.jianshu.com/p/fc59cb61f7cc
useEffect(() => {
console.log("modify vars")
modifyVars(system.dark, system.primary)
return () => {}
})
return (
<>
<Intl>
<ConfigProvider>
<Switch>
<Route path="/home" component={Home} />
<Route path="/login" exact component={Login} />
<Redirect to="/home"/>
</Switch>
</ConfigProvider>
</Intl>
</>
)
}))
export default App

@ -0,0 +1,424 @@
import React, { useState, useEffect} from 'react'
import { inject, observer } from 'mobx-react'
import { Spin, Row, Col, Card } from 'antd'
import {
Chart,
Geom,
Axis,
Tooltip,
Coord,
Legend
} from 'bizcharts'
import DataSet from '@antv/data-set'
//
const Groupedcolumn = ()=>{
const data = [
{
name: 'London',
'1月': 18.9,
'2月': 28.8,
'3月': 39.3,
'4月': 81.4,
'5月': 47,
'6月': 20.3,
'7月': 24,
'8月': 35.6,
'9月': 5.6,
'10月': 1.6,
'11月': 31.6,
'12月': 13.6
},
{
name: 'Berlin',
'1月': 12.9,
'2月': 2.8,
'3月': 43.3,
'4月': 1.4,
'5月': 90,
'6月': 50.3,
'7月': 21,
'8月': 15.6,
'9月': 12.6,
'10月': 23.6,
'11月': 21.6,
'12月': 54.6
}
]
const ds = new DataSet()
const dv = ds.createView().source(data)
dv.transform({
type: 'fold',
fields: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
//
key: '月份',
// key
value: '月均降雨量' // value
})
return (
<>
<Chart height={200} padding='auto' data={dv} forceFit>
<Tooltip
crosshairs={{
type: 'y'
}}
/>
<Geom
type='interval'
position='月份*月均降雨量'
color={'name'}
adjust={[
{
type: 'dodge',
marginRatio: 1 / 32
}
]}
/>
</Chart>
</>
)
}
//
const Basiccolumn = ()=>{
const data = [
{
year: '一月',
sales: 38
},
{
year: '二月',
sales: 52
},
{
year: '三月',
sales: 61
},
{
year: '四月',
sales: 145
},
{
year: '五月',
sales: 48
},
{
year: '六月',
sales: 38
},
{
year: '七月',
sales: 38
},
{
year: '八月',
sales: 38
}
]
const cols = {
sales: {
tickInterval: 20
}
}
return (
<>
<Chart height={200} padding='auto' data={data} scale={cols} forceFit>
<Axis name='sales' />
<Tooltip
crosshairs={{
type: 'y'
}}
/>
<Geom type='interval' position='year*sales' />
</Chart>
</>
)
}
//
const Range = () => {
const data = [
{
profession: '两年制副学士学位',
highest: 110000,
minimum: 23000,
mean: 56636
},
{
profession: '执法与救火',
highest: 120000,
minimum: 18000,
mean: 66625
},
{
profession: '教育学',
highest: 125000,
minimum: 24000,
mean: 72536
},
{
profession: '心理学',
highest: 130000,
minimum: 22500,
mean: 75256
},
{
profession: '计算机科学',
highest: 131000,
minimum: 23000,
mean: 77031
}
]
const ds = new DataSet()
const dv = ds.createView().source(data)
dv.transform({
type: 'map',
callback(row) {
//
row.range = [row.minimum, row.highest]
return row
}
})
return (
<>
<Chart height={200} padding='auto' data={dv} forceFit>
<Coord transpose />
<Axis
name='profession'
label={{
offset: 12
}}
/>
<Axis name='range' />
<Tooltip />
<Geom type='interval' position='profession*range' />
</Chart>
</>
)
}
//
const Stackedpercentagecolumn = ()=>{
const data = [
{
country: 'Europe',
year: '1750',
value: 163
},
{
country: 'Europe',
year: '1800',
value: 203
},
{
country: 'Europe',
year: '1850',
value: 276
},
{
country: 'Europe',
year: '1900',
value: 408
},
{
country: 'Europe',
year: '1950',
value: 547
},
{
country: 'Europe',
year: '1999',
value: 729
},
{
country: 'Europe',
year: '2050',
value: 628
},
{
country: 'Europe',
year: '2100',
value: 828
},
{
country: 'Asia',
year: '1750',
value: 502
},
{
country: 'Asia',
year: '1800',
value: 635
},
{
country: 'Asia',
year: '1850',
value: 809
},
{
country: 'Asia',
year: '1900',
value: 947
},
{
country: 'Asia',
year: '1950',
value: 1402
},
{
country: 'Asia',
year: '1999',
value: 3634
},
{
country: 'Asia',
year: '2050',
value: 5268
},
{
country: 'Asia',
year: '2100',
value: 7268
}
]
const ds = new DataSet()
const dv = ds
.createView()
.source(data)
.transform({
type: 'percent',
field: 'value',
//
dimension: 'country',
//
groupBy: ['year'],
//
as: 'percent'
})
const cols = {
percent: {
min: 0,
formatter(val) {
return (val * 100).toFixed(2) + '%'
}
}
}
return (
<>
<Chart height={200} padding='auto' data={dv} scale={cols} forceFit>
<Legend />
<Axis name='year' />
<Axis name='percent' />
<Tooltip />
<Geom
type='intervalStack'
position='year*percent'
color={'country'}
/>
</Chart>
</>
)
}
//
const Stackedcolumn = ()=>{
const data = [
{
name: 'London',
'Jan.': 18.9,
'Feb.': 28.8,
'Mar.': 39.3,
'Apr.': 81.4,
May: 47,
'Jun.': 20.3,
'Jul.': 24,
'Aug.': 35.6
},
{
name: 'Berlin',
'Jan.': 12.4,
'Feb.': 23.2,
'Mar.': 34.5,
'Apr.': 99.7,
May: 52.6,
'Jun.': 35.5,
'Jul.': 37.4,
'Aug.': 42.4
}]
const ds = new DataSet()
const dv = ds.createView().source(data)
dv.transform({
type: 'fold',
fields: ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.'],
//
key: '月份',
// key
value: '月均降雨量' // value
})
return (
<>
<Chart height={200} padding="auto" data={dv} forceFit>
<Legend />
<Axis name='月份' />
<Axis name='月均降雨量' />
<Tooltip />
<Geom
type='intervalStack'
position='月份*月均降雨量'
color={'name'}
style={{
stroke: '#fff',
lineWidth: 1
}}
/>
</Chart>
</>
)
}
const Charts = inject('system')(observer((props) =>{
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(false)
return () => {}
}, [loading])
return (
<Spin spinning={loading}>
<div className='container'>
<Row gutter={24}>
<Col span={24}>
<Card title='条形图' style={{borderRadius:'10px'}}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} className='gutter-row'>
<Groupedcolumn />
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8} className='gutter-row' span={8}>
<Basiccolumn />
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8} className='gutter-row' span={8}>
<Range />
</Col>
</Card>
</Col>
<Col span={24} style={{marginTop: '20px'}}>
<Card title='堆叠柱形图' style={{borderRadius:'10px'}}>
<Col xs={24} sm={24} md={12} lg={12} xl={12} className='gutter-row' span={8}>
<Stackedpercentagecolumn />
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12} className='gutter-row' span={8}>
<Stackedcolumn />
</Col>
</Card>
</Col>
<Col span={24} style={{marginTop: '20px'}}>
<Card title='横图表' style={{borderRadius:'10px'}}>
<Col xs={24} sm={24} md={12} lg={12} xl={12} className='gutter-row' span={8}>
<Stackedpercentagecolumn />
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12} className='gutter-row' span={8}>
<Stackedcolumn />
</Col>
</Card>
</Col>
</Row>
</div>
</Spin>
)
}))
export default Charts

@ -0,0 +1,97 @@
import React, { useState, useEffect} from 'react'
import { inject, observer } from 'mobx-react'
import { Spin, Row, Col, Card } from 'antd'
import {
Chart,
Geom,
Axis,
Tooltip
} from 'bizcharts'
const Dashboard = inject('system')(observer(() =>{
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(false)
return () => {}
}, [])
const data = [
{
year: '1991',
value: 3
},
{
year: '1992',
value: 4
},
{
year: '1993',
value: 3.5
},
{
year: '1994',
value: 5
},
{
year: '1995',
value: 4.9
},
{
year: '1996',
value: 6
},
{
year: '1997',
value: 7
},
{
year: '1998',
value: 9
},
{
year: '1999',
value: 13
}
]
const cols = {
value: {
min: 0
},
year: {
range: [0, 1]
}
}
return (
<Spin spinning={loading} tip='Loading...'>
<div className='container'>
<Row gutter={24}>
<Col xs={24} sm={24} md={12} lg={12} xl={12} className='gutter-row'>
<Card style={{borderRadius:'10px'}}>
<Chart padding="auto" height={278} data={data} scale={cols} forceFit>
<Axis name='year' />
<Axis name='value' />
<Tooltip
crosshairs={{
type: 'y'
}}
/>
<Geom type='line' position='year*value' size={2} />
<Geom
type='point'
position='year*value'
size={4}
shape={'circle'}
style={{
stroke: '#fff',
lineWidth: 1
}}
/>
</Chart>
</Card>
</Col>
</Row>
</div>
</Spin>
)
}))
export default Dashboard

@ -0,0 +1,20 @@
import React, { useState, useEffect} from 'react'
import { inject, observer } from 'mobx-react'
import { Spin } from 'antd'
const A = inject('system')(observer(() =>{
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(false)
return () => {}
}, [])
return (
<Spin spinning={loading} tip="Loading...">
<div className='container'>
s
</div>
</Spin>
)
}))
export default A

@ -0,0 +1,94 @@
import React, { useState, useEffect } from 'react'
import { Spin } from 'antd'
function B() {
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(false)
return () => {}
}, [])
return (
<Spin spinning={loading} tip="Loading...">
<div>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
<p>ds</p>
</div>
</Spin>
)
}
export default B

@ -0,0 +1,45 @@
import React, { useEffect, useState } from 'react'
import { Spin, Row, Col, Card, Collapse } from 'antd'
const { Panel } = Collapse
function Accordion() {
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(false)
return () => {}
}, [])
const text = `
A dog is a type of domesticated animal.
Known for its loyalty and faithfulness,
it can be found as a welcome guest in many households across the world.
`
return (
<Spin spinning={loading} tip="Loading...">
<div className='container'>
<Row gutter={[8, 8]}>
<Col xs={24} sm={24} md={24} lg={12} xl={12}>
<Card title='折叠面板' style={{borderRadius:'10px'}}>
<Collapse defaultActiveKey={['1']} onChange={ ()=>{} }>
<Panel header="This is panel header 1" key="1">
<p>{text}</p>
</Panel>
<Panel header="This is panel header 2" key="2">
<p>{text}</p>
</Panel>
<Panel header="This is panel header 3" key="3" disabled>
<p>{text}</p>
</Panel>
</Collapse>
</Card>
</Col>
<Col xs={24} sm={24} md={24} lg={12} xl={12}>
<Card title='更多分页' style={{borderRadius:'10px'}}>
</Card>
</Col>
</Row>
</div>
</Spin>
)
}
export default Accordion

@ -0,0 +1,19 @@
import React, { useEffect, useState } from 'react'
import { Spin } from 'antd'
function DatePickers() {
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(false)
return () => {}
}, [])
return (
<Spin spinning={loading} tip="Loading...">
<div className='container'>
DatePicker
</div>
</Spin>
)
}
export default DatePickers

@ -0,0 +1,19 @@
import React, { useEffect, useState } from 'react'
import { Spin } from 'antd'
function Icons() {
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(false)
return () => {}
}, [])
return (
<Spin spinning={loading} tip="Loading...">
<div className='container'>
Icons
</div>
</Spin>
)
}
export default Icons

@ -0,0 +1,53 @@
import React, { useEffect, useState } from 'react'
import { Spin, Row, Col, Card, Pagination } from 'antd'
function Paginations() {
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(false)
return () => {}
}, [])
function onShowSizeChange(current, pageSize) {
console.log(current, pageSize)
}
return (
<Spin spinning={loading} tip="Loading...">
<div className='container'>
<Row gutter={[8, 8]}>
<Col xs={24} sm={24} md={24} lg={12} xl={12}>
<Card title='基本分页' style={{borderRadius:'10px'}}>
<Pagination defaultCurrent={1} total={50} />
</Card>
</Col>
<Col xs={24} sm={24} md={24} lg={12} xl={12}>
<Card title='更多分页' style={{borderRadius:'10px'}}>
<Pagination defaultCurrent={6} total={500} />
</Card>
</Col>
</Row>
<Row gutter={[8, 8]}>
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
<Card title='改变每页显示条目数' style={{borderRadius:'10px'}}>
<Pagination
showSizeChanger
onShowSizeChange={onShowSizeChange}
defaultCurrent={3}
total={500}
/>
<br />
<Pagination
showSizeChanger
onShowSizeChange={onShowSizeChange}
defaultCurrent={3}
total={500}
disabled
/>
</Card>
</Col>
</Row>
</div>
</Spin>
)
}
export default Paginations

@ -0,0 +1,29 @@
@import '../../styles/main.less';
.app{
:global {
.ant-menu-inline{
border-right: none;
}
.ant-menu-horizontal{
line-height: 64px;
}
.ant-menu-vertical .ant-menu-item:not(:last-child), .ant-menu-inline .ant-menu-item:not(:last-child) {
margin-bottom: 0;
}
.ant-menu-inline .ant-menu-item:first-child{
margin-top: 0;
}
.ant-menu-item:first-child{
margin-top: 0;
}
// less 编译导致antd 样式 ~"100%/100%" 计算错误,重新覆盖
.ant-back-top-icon {
width: 14px;
height: 16px;
margin: 12px auto;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAoCAYAAACWwljjAAAABGdBTUEAALGPC/xhBQAAAbtJREFUWAntmMtKw0AUhhMvS5cuxILgQlRUpIggIoKIIoigG1eC+AA+jo+i6FIXBfeuXIgoeKVeitVWJX5HWhhDksnUpp3FDPyZk3Nm5nycmZKkXhAEOXSA3lG7muTeRzmfy6HneUvIhnYkQK+Q9NhAA0Opg0vBEhjBKHiyb8iGMyQMOYuK41BcBSypAL+MYXSKjtFAW7EAGEO3qN4uMQbbAkXiSfRQJ1H6a+yhlkKRcAoVFYiweYNjtCVQJJpBz2GCiPt7fBOZQpFgDpUikse5HgnkM4Fi4QX0Fpc5wf9EbLqpUCy4jMoJSXWhFwbMNgWKhVbRhy5jirhs9fy/oFhgHVVTJEs7RLZ8sSEoJm6iz7SZDMbJ+/OKERQTttCXQRLToRUmrKWCYuA2+jbN0MB4OQobYShfdTCgn/sL1K36M7TLrN3n+758aPy2rrpR6+/od5E8tf/A1uLS9aId5T7J3CNYihkQ4D9PiMdMC7mp4rjB9kjFjZp8BlnVHJBuO1yFXIV0FdDF3RlyFdJVQBdv5AxVdIsq8apiZ2PyYO1EVykesGfZEESsCkweyR8MUW+V8uJ1gkYipmpdP1pm2aJVPEGzAAAAAElFTkSuQmCC) ~"100%/100%" no-repeat;
}
}
}

@ -0,0 +1,49 @@
import React, { useEffect } from 'react'
import { withRouter, useHistory } from 'react-router-dom'
import { Layout } from 'antd'
import { useMediaQuery } from 'react-responsive'
import style from './index.module.less'
import { inject, observer } from 'mobx-react'
import MenuWrapper from '../../layouts/Menu'
import DrawerWrapper from '../../layouts/Drawer'
import ContentWrapper from '../../layouts/Content'
import HeaderWrapper from '../../layouts/Header'
import { getToken } from '../../library/utils/auth'
import { isEmpty } from '../../library/utils/validate'
import Routers from '../../library/routes'
const { Sider } = Layout
let Index = inject('system')(observer((props:any) => {
let {system} = props
// const isMobile = useMediaQuery({
// query: '(max-device-width: 991px)'
// })
let history = useHistory()
useEffect(() => {
if(isEmpty(getToken())){
history.push('/login')
}
return () => {}
})
const isDesktop = useMediaQuery({
query: '(min-device-width: 992px)'
})
return (
<Layout className={`${style.app} ${system.dark?style.dark:''}`}>
{isDesktop &&
<Sider collapsed={system.collapsed}>
<MenuWrapper routers={Routers}/>
</Sider>
}
<Layout>
<HeaderWrapper />
<ContentWrapper routers={Routers}/>
<DrawerWrapper />
</Layout>
</Layout>
)
}))
export default withRouter(Index)

@ -0,0 +1,105 @@
import React, {useState} from 'react'
import { Button, Form, Input, Icon, message, Spin } from 'antd'
import { useHistory} from 'react-router-dom'
import style from './index.module.less'
import { post } from '@http/index'
import { setToken } from '@utils/auth'
import storage from '@utils/localStorage'
import { inject, observer } from 'mobx-react'
function FormBox(props) {
let { user } = props
let history = useHistory()
const [btnLoading, setBtnLoading] = useState(false)
const { getFieldDecorator } = props.form
function handleSubmit(e){
e.preventDefault()
props.form.validateFields((err, values) => {
setBtnLoading(true)
post('/api/login',
{
username:'admin',
password: '123456'
},
function(res){
if(res.err === 1){
message.error(res.msg)
}else{
setToken(res.data.token)
delete res.data.token
storage.set('info', res.data)
props.setloading(true)
user.setInfo(res.data)
setTimeout(() => {
props.setloading(false)
history.push('/home/dashboard')
}, 2000)
}
},
function(err){
console.log(err)
message.error('ds')
},
function(){
setBtnLoading(false)
}
)
// if (!err) {
// history.push("/index");
// }
})
}
return (
<div>
<Form className="login-form">
<Form.Item>
{getFieldDecorator('username', {
initialValue: 'dsa',
rules: [{ required: true, message: '用户名不能为空' }]
})(
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="Username"
/>
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('password', {
initialValue: 'dsa',
rules: [{ required: true, message: '密码不能为空' }]
})(
<Input.Password
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="Password"
/>
)}
</Form.Item>
<Form.Item labelAlign='right' labelCol={{span: 3, offset: 12}}>
<Button onClick={handleSubmit} type="primary" shape="round" loading={btnLoading?true:false}>
登录
</Button>
</Form.Item>
</Form>
</div>
)
}
const FormBoxWrapper = Form.create({ name: 'normal_login' })(inject('user')(observer(FormBox)))
let Login = inject('user')(observer(() => {
const [loading, setloading] = useState(false)
//npm install --save rc-form-hooks
// https://www.jianshu.com/p/fc59cb61f7cc
return (
<div className={style.app}>
<Spin spinning={loading}>
<div className={style.rectangle}>
<FormBoxWrapper setloading={setloading}/>
</div>
</Spin>
</div>
)
}))
export default Login

@ -0,0 +1,22 @@
.app{
width: 100vw;
height: 100vh;
background: #f3f6f9;
display: flex;
justify-content: center;
align-items: center;
.rectangle{
width: 360px;
background-color: #ffffff;
box-shadow: 0px 2px 4px 0px
#d0d0d0;
border-radius: 10px;
padding: 20px;
}
:global {
.ant-form-item{
margin-bottom: 0;
}
}
}

@ -0,0 +1,14 @@
/* eslint-disable react-hooks/rules-of-hooks */
import React from 'react'
import {Responsive, useMediaQuery} from 'react-responsive'
const Desktop = props => <Responsive {...props} minWidth={768} />
const Mobile = props => <Responsive {...props} maxWidth={767} />
const isMobile = useMediaQuery({
query: '(max-device-width: 991px)'
})
export {
Desktop,
Mobile,
isMobile
}

@ -0,0 +1 @@
/// <reference types="react-scripts" />

@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
/* eslint-disable */
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

@ -1,20 +1,7 @@
import Vue from 'vue' import system from './system'
import Vuex from 'vuex' import user from './user'
import misc from './modules/misc'
import user from './modules/user'
import post from './modules/post'
import getters from './getters'
Vue.use(Vuex) export default {
system,
const store = new Vuex.Store({ user
modules: { }
misc,
user,
post
},
getters
})
export default store

@ -0,0 +1,47 @@
import { observable, action } from 'mobx'
class System{
// constructor() {
// }
@observable dark = false
@observable collapsed = false
@observable drawer = false
@observable mode = 'inline'
@observable theme = 'light'
@observable primary = '#2196f3'
@observable locale = 'zh_CN'
@observable lang = 'zh'
@action
setDark = () => {
this.dark = !this.dark
if(this.dark){
this.theme = 'dark'
}else{
this.theme = 'light'
}
}
@action
setCollapsed = () => {
this.collapsed = !this.collapsed
}
@action
setDrawer = () => {
this.drawer = !this.drawer
}
@action
setPrimary = (color) => {
this.primary = color
}
@action
setLocale = (locale) => {
this.locale = locale
}
@action
setLang = (lang) => {
this.lang = lang
}
}
export default new System()

@ -0,0 +1,12 @@
import { action, observable } from 'mobx'
class User{
@observable info = new Map()
@action
setInfo = (info) => {
this.info.replace(info)
}
}
export default new User()

@ -0,0 +1,13 @@
@import "./vars.less";
:global {
.ant-layout{
width: 100vw;
height: 100%;
min-height: 100vh;
}
.container{
min-height: calc(~ '100vh - 64px - 33px');
padding: 20px 20px;
}
}

@ -0,0 +1,40 @@
/* eslint-disable quotes */
let colors = require('@ant-design/colors')
let darkTheme = {
'@primary-color': '#0A53B0',
'@layout-body-background': '#171717',
'@background-color-base': '#262626',
'@body-background': '#404041',
'@layout-sider-background': '#171F22',
'@component-background': '#171F22',
'@layout-header-background': '#171F22',
'@menu-dark-submenu-bg': '#171F22',
'@input-bg': '#313133',
'@btn-default-bg': '#262626',
'@border-color-base': 'rgba(255, 255, 255, 0.25)',
'@border-color-split': '#363636',
'@heading-color': '#E3E3E3',
'@text-color': '#E3E3E3',
'@text-color-secondary': 'fade(#fff, 65%)',
'@table-selected-row-bg': '#3a3a3a',
'@table-expanded-row-bg': '#3b3b3b',
'@table-header-bg': '#3a3a3b',
'@table-row-hover-bg': '#3a3a3b',
'@layout-trigger-color': 'fade(#fff, 80%)',
'@layout-trigger-background': '#313232',
'@alert-message-color': 'fade(#000, 67%)',
'@item-hover-bg': "fade(" + colors.blue[5] + ", 20%)",
'@item-active-bg': "fade(" + colors.blue[5] + ", 40%)",
'@disabled-color': 'rgba(255, 255, 255, 0.25)',
'@tag-default-bg': '#262628',
'@popover-bg': '#262629',
'@wait-icon-color': 'fade(#fff, 64%)',
'@background-color-light': "fade(" + colors.blue[5] + ", 40%)",
'@collapse-header-bg': '#262629',
'@info-color': '#313133',
'@highlight-color': colors.red[7],
'@warning-color': colors.gold[9],
'@menu-dark-highlight-color': "#fff"
}
export default darkTheme

@ -0,0 +1,27 @@
import dark from './dark'
import light from './light'
const darkTheme = {}
const lightTheme = {}
let Light = (primary) => {
Object.keys(light).forEach((key) => {
lightTheme[`${key}`] = light[key]
})
lightTheme['@primary-color'] = primary
return lightTheme
}
let Dark = (primary) => {
Object.keys(dark).forEach((key) => {
darkTheme[`${key}`] = dark[key]
})
darkTheme['@primary-color'] = primary
return darkTheme
}
export {
Light,
Dark
}

@ -0,0 +1,428 @@
/* eslint-disable quotes */
let lightTheme = {
'@ant-prefix': "ant",
'@iconfont-css-prefix': "anticon",
'@primary-color': "@blue-6",
'@success-color': "@green-6",
'@info-color': "@blue-6",
'@warning-color': "@gold-6",
'@error-color': "@red-5",
'@highlight-color': "@red-5",
'@processing-color': "@blue-6",
'@body-background': "#fff",
'@component-background': "#fff",
'@icon-color': "inherit",
'@icon-color-hover': "fade(@black, 75%)",
'@heading-color': "fade(#000, 85%)",
'@text-color': "fade(@black, 65%)",
'@text-color-secondary': "fade(@black, 45%)",
'@text-color-inverse': "@white",
'@heading-color-dark': "fade(@white, 100%)",
'@text-color-dark': "fade(@white, 85%)",
'@text-color-secondary-dark': "fade(@white, 65%)",
'@text-selection-bg': "@primary-color",
'@border-color-base': "hsv(0, 0, 85%) ",
'@border-color-split': "hsv(0, 0, 94%) ",
'@border-color-inverse': "@white",
'@border-width-base': "1px",
'@border-style-base': "solid ",
//
'@layout-body-background': "#f0f2f5",
'@layout-header-background': "#FFF",
'@layout-footer-background': "@layout-body-background",
'@layout-header-height': "64px",
'@layout-header-padding': "0 50px",
'@layout-footer-padding': "24px 50px",
'@layout-sider-background': "@layout-header-background",
'@layout-trigger-height': "48px",
'@layout-trigger-background': "#002140",
'@layout-trigger-color': "#fff",
'@layout-zero-trigger-width': "36px",
'@layout-zero-trigger-height': "42px",
'@layout-sider-background-light': "#fff",
'@layout-trigger-background-light': "#fff",
'@layout-trigger-color-light': "@text-color",
'@font-family': "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',",
'@code-family': "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
'@font-variant-base': "tabular-nums",
'@font-feature-settings-base': "'tnum'",
'@font-size-base': "14px",
'@font-size-sm': "12px",
'@line-height-base': "1.5",
'@border-radius-base': "2px",
'@border-radius-sm': "2px",
'@padding-lg': "24px",
'@padding-md': "16px",
'@padding-sm': "12px",
'@padding-xs': "8px",
'@disabled-color': "fade(#000, 25%)",
'@disabled-bg': "@background-color-base",
'@disabled-color-dark': "fade(#fff, 35%)",
'@link-color': "@primary-color",
'@link-decoration': "none",
'@link-hover-decoration': "none",
'@outline-blur-size': "0px",
'@outline-width': "2px",
'@outline-color': "@primary-color",
'@background-color-light': "hsv(0, 0, 98%) ",
'@background-color-base': "hsv(0, 0, 96%) ",
'@item-active-bg': "@primary-1",
'@item-hover-bg': "#f5f5f5",
'@shadow-color': "rgba(0, 0, 0, 0.15)",
'@shadow-color-inverse': "@component-background",
'@box-shadow-base': "@shadow-2",
'@shadow-1-up': "0 -6px 16px -8px rgba(0, 0, 0, 0.08), 0 -9px 28px 0 rgba(0, 0, 0, 0.05),",
'@shadow-1-down': "0 6px 16px -8px rgba(0, 0, 0, 0.08), 0 9px 28px 0 rgba(0, 0, 0, 0.05),",
'@shadow-1-left': "-6px 0 16px -8px rgba(0, 0, 0, 0.08), -9px 0 28px 0 rgba(0, 0, 0, 0.05),",
'@shadow-1-right': "6px 0 16px -8px rgba(0, 0, 0, 0.08), 9px 0 28px 0 rgba(0, 0, 0, 0.05),",
'@shadow-2': "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),",
'@btn-font-weight': "400",
'@btn-border-radius-base': "@border-radius-base",
'@btn-border-radius-sm': "@border-radius-base",
'@btn-border-width': "@border-width-base",
'@btn-border-style': "@border-style-base",
'@btn-shadow': "0 2px 0 rgba(0, 0, 0, 0.015)",
'@btn-primary-shadow': "0 2px 0 rgba(0, 0, 0, 0.045)",
'@btn-text-shadow': "0 -1px 0 rgba(0, 0, 0, 0.12)",
'@btn-primary-color': "#fff",
'@btn-primary-bg': "@primary-color",
'@btn-default-color': "@text-color",
'@btn-default-bg': "@component-background",
'@btn-default-border': "@border-color-base",
'@btn-danger-color': "#fff",
'@btn-danger-bg': "@error-color",
'@btn-danger-border': "@error-color",
'@btn-disable-color': "@disabled-color",
'@btn-disable-bg': "@disabled-bg",
'@btn-disable-border': "@border-color-base",
'@btn-padding-base': "0 @padding-md - 1px",
'@btn-font-size-lg': "@font-size-lg",
'@btn-font-size-sm': "@font-size-base",
'@btn-padding-lg': "@btn-padding-base",
'@btn-padding-sm': "0 @padding-xs - 1px",
'@btn-height-base': "32px",
'@btn-height-lg': "40px",
'@btn-height-sm': "24px",
'@btn-circle-size': "@btn-height-base",
'@btn-circle-size-lg': "@btn-height-lg",
'@btn-circle-size-sm': "@btn-height-sm",
'@btn-group-border': "@primary-5",
'@checkbox-size': "16px",
'@checkbox-color': "@primary-color",
'@checkbox-check-color': "#fff",
'@checkbox-border-width': "@border-width-base",
'@dropdown-selected-color': "@primary-color",
'@dropdown-vertical-padding': "5px",
'@dropdown-line-height': "22px",
'@dropdown-font-size': "@font-size-base",
'@empty-font-size': "@font-size-base",
'@radio-size': "16px",
'@radio-dot-color': "@primary-color",
'@radio-button-bg': "@btn-default-bg",
'@radio-button-checked-bg': "@btn-default-bg",
'@radio-button-color': "@btn-default-color",
'@radio-button-hover-color': "@primary-5",
'@radio-button-active-color': "@primary-7",
'@screen-xs': "480px",
'@screen-xs-min': "@screen-xs",
'@screen-sm': "576px",
'@screen-sm-min': "@screen-sm",
'@screen-md': "768px",
'@screen-md-min': "@screen-md",
'@screen-lg': "992px",
'@screen-lg-min': "@screen-lg",
'@screen-xl': "1200px",
'@screen-xl-min': "@screen-xl",
'@screen-xxl': "1600px",
'@screen-xxl-min': "@screen-xxl",
'@grid-columns': "24",
'@grid-gutter-width': "0",
'@zindex-table-fixed': "auto",
'@zindex-affix': "10",
'@zindex-back-top': "10",
'@zindex-badge': "10",
'@zindex-picker-panel': "10",
'@zindex-popup-close': "10",
'@zindex-modal': "1000",
'@zindex-modal-mask': "1000",
'@zindex-message': "1010",
'@zindex-notification': "1010",
'@zindex-popover': "1030",
'@zindex-dropdown': "1050",
'@zindex-picker': "1050",
'@zindex-tooltip': "1060",
'@animation-duration-slow': "0.3s",
'@animation-duration-base': "0.2s",
'@animation-duration-fast': "0.1s",
'@collapse-panel-border-radius': "@border-radius-base",
'@label-required-color': "@highlight-color",
'@label-color': "@heading-color",
'@form-warning-input-bg': "@input-bg",
'@form-item-margin-bottom': "24px",
'@form-item-trailing-colon': "true",
'@form-vertical-label-padding': "0 0 8px",
'@form-vertical-label-margin': "0",
'@form-error-input-bg': "@input-bg",
'@input-height-base': "32px",
'@input-height-lg': "40px",
'@input-height-sm': "24px",
'@input-padding-vertical-base': "4px",
'@input-padding-vertical-sm': "1px",
'@input-padding-vertical-lg': "6px",
'@input-placeholder-color': "hsv(0, 0, 75%)",
'@input-color': "@text-color",
'@input-border-color': "@border-color-base",
'@input-bg': "@component-background",
'@input-number-handler-active-bg': "#f4f4f4",
'@input-addon-bg': "@background-color-light",
'@input-hover-border-color': "@primary-5",
'@input-disabled-bg': "@disabled-bg",
'@input-outline-offset': "0 0",
'@select-border-color': "@border-color-base",
'@select-item-selected-font-weight': "600px",
'@tooltip-max-width': "250px",
'@tooltip-color': "#fff",
'@tooltip-bg': "rgba(0, 0, 0, 0.75)",
'@tooltip-arrow-width': "5px",
'@tooltip-distance': "@tooltip-arrow-width - 1px + 4px",
'@tooltip-arrow-color': "@tooltip-bg",
'@popover-bg': "@component-background",
'@popover-color': "@text-color",
'@popover-min-width': "177px",
'@popover-arrow-width': "6px",
'@popover-arrow-color': "@popover-bg",
'@popover-arrow-outer-color': "@popover-bg",
'@popover-distance': "@popover-arrow-width + 4px",
'@modal-body-padding': "24px",
'@modal-header-bg': "@component-background",
'@modal-footer-bg': "transparent",
'@modal-mask-bg': "fade(@black, 45%)",
'@progress-default-color': "@processing-color",
'@progress-remaining-color': "@background-color-base",
'@progress-text-color': "@text-color",
'@progress-radius': "100px",
'@menu-inline-toplevel-item-height': "40px",
'@menu-item-height': "40px",
'@menu-collapsed-width': "80px",
'@menu-bg': "@component-background",
'@menu-popup-bg': "@component-background",
'@menu-item-color': "@text-color",
'@menu-highlight-color': "@primary-color",
'@menu-item-active-bg': "@primary-1",
'@menu-item-active-border-width': "3px",
'@menu-item-group-title-color': "@text-color-secondary",
'@menu-dark-color': "@text-color-secondary-dark",
'@menu-dark-bg': "@layout-header-background",
'@menu-dark-arrow-color': "#fff",
'@menu-dark-submenu-bg': "#000c17",
'@menu-dark-highlight-color': "#fff",
'@menu-dark-item-active-bg': "@primary-color",
'@spin-dot-size-sm': "14px",
'@spin-dot-size': "20px",
'@spin-dot-size-lg': "32px",
'@table-header-bg': "@background-color-light",
'@table-header-color': "@heading-color",
'@table-header-sort-bg': "@background-color-base",
'@table-body-sort-bg': "rgba(0, 0, 0, 0.01)",
'@table-row-hover-bg': "@item-hover-bg",
'@table-selected-row-color': "inherit",
'@table-selected-row-bg': "@primary-1",
'@table-body-selected-sort-bg': "@table-selected-row-bg",
'@table-selected-row-hover-bg': "@table-selected-row-bg",
'@table-expanded-row-bg': "#fbfbfb",
'@table-padding-vertical': "16px",
'@table-padding-horizontal': "16px",
'@table-border-radius-base': "@border-radius-base",
'@tag-default-bg': "@background-color-light",
'@tag-default-color': "@text-color",
'@tag-font-size': "@font-size-sm",
'@time-picker-panel-column-width': "56px",
'@time-picker-panel-width': "@time-picker-panel-column-width * 3",
'@time-picker-selected-bg': "@item-active-bg",
'@carousel-dot-width': "16px",
'@carousel-dot-height': "3px",
'@carousel-dot-active-width': "24px",
'@badge-color': "@error-color",
'@badge-height': "20px",
'@badge-dot-size': "6px",
'@badge-font-size': "@font-size-sm",
'@badge-font-weight': "normal",
'@badge-status-size': "6px",
'@badge-text-color': "@component-background",
'@rate-star-color': "@yellow-6",
'@rate-star-bg': "@border-color-split",
'@card-head-color': "@heading-color",
'@card-head-background': "transparent",
'@card-head-padding': "16px",
'@card-inner-head-padding': "12px",
'@card-padding-base': "24px",
'@card-actions-background': "@background-color-light",
'@card-background': "@component-background",
'@card-shadow': "0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12),",
'@card-radius': "@border-radius-base",
'@comment-padding-base': "16px 0",
'@comment-nest-indent': "44px",
'@comment-font-size-base': "@font-size-base",
'@comment-font-size-sm': "@font-size-sm",
'@comment-author-name-color': "@text-color-secondary",
'@comment-author-time-color': "#ccc",
'@comment-action-color': "@text-color-secondary",
'@comment-action-hover-color': "#595959",
'@tabs-card-head-background': "@background-color-light",
'@tabs-card-height': "40px",
'@tabs-card-active-color': "@primary-color",
'@tabs-title-font-size': "@font-size-base",
'@tabs-title-font-size-lg': "@font-size-lg",
'@tabs-title-font-size-sm': "@font-size-base",
'@tabs-ink-bar-color': "@primary-color",
'@tabs-bar-margin': "0 0 16px 0",
'@tabs-horizontal-margin': "0 32px 0 0",
'@tabs-horizontal-padding': "12px 16px",
'@tabs-horizontal-padding-lgr': "16px",
'@tabs-horizontal-padding-sm': "8px 16px",
'@tabs-vertical-padding': "8px 24px",
'@tabs-vertical-margin': "0 0 16px 0",
'@tabs-scrolling-size': "32px",
'@tabs-highlight-color': "@primary-color",
'@tabs-hover-color': "@primary-5",
'@tabs-active-color': "@primary-7",
'@tabs-card-gutter': "2px",
'@tabs-card-tab-active-border-top': "2px solid transparent",
'@back-top-color': "#fff",
'@back-top-bg': "@text-color-secondary",
'@back-top-hover-bg': "@text-color",
'@avatar-size-base': "32px",
'@avatar-size-lg': "40px",
'@avatar-size-sm': "24px",
'@avatar-font-size-base': "18px",
'@avatar-font-size-lg': "24px",
'@avatar-font-size-sm': "14px",
'@avatar-bg': "#ccc",
'@avatar-color': "#fff",
'@avatar-border-radius': "@border-radius-base",
'@switch-height': "22px",
'@switch-sm-height': "16px",
'@switch-sm-checked-margin-left': "-(@switch-sm-height - 3px)",
'@switch-disabled-opacity': "0.4",
'@switch-color': "@primary-color",
'@switch-shadow-color': "fade(#00230b, 20%)",
'@pagination-item-size': "32px",
'@pagination-item-size-sm': "24px",
'@pagination-font-weight-active': "500",
'@pagination-font-family': "Arial",
'@pagination-item-bg-active': "@component-background",
'@page-header-padding-horizontal': "24px",
'@page-header-padding-vertical': "16px",
'@breadcrumb-base-color': "@text-color-secondary",
'@breadcrumb-last-item-color': "@text-color",
'@breadcrumb-font-size': "@font-size-base",
'@breadcrumb-icon-font-size': "@font-size-base",
'@breadcrumb-link-color': "@text-color-secondary",
'@breadcrumb-link-color-hover': "@primary-5",
'@breadcrumb-separator-color': "@text-color-secondary",
'@breadcrumb-separator-margin': "0 @padding-xs",
'@slider-handle-border-width': "2px",
'@slider-handle-shadow': "0",
'@slider-margin': "14px 6px 10px",
'@slider-rail-background-color': "@background-color-base",
'@slider-rail-background-color-hover': "#e1e1e1",
'@slider-track-background-color': "@primary-3",
'@slider-track-background-color-hover': "@primary-4",
'@slider-handle-color': "@primary-3",
'@slider-handle-color-hover': "@primary-4",
'@slider-handle-color-focus': "tint(@primary-color, 20%)",
'@slider-handle-color-focus-shadow': "fade(@primary-color, 20%)",
'@slider-handle-color-tooltip-open': "@primary-color",
'@slider-dot-border-color': "@border-color-split",
'@slider-dot-border-color-active': "tint(@primary-color, 50%)",
'@slider-disabled-color': "@disabled-color",
'@slider-disabled-background-color': "@component-background",
'@tree-title-height': "24px",
'@tree-child-padding': "18px",
'@tree-directory-selected-color': "#fff",
'@tree-directory-selected-bg': "@primary-color",
'@collapse-header-padding': "12px 16px",
'@collapse-header-padding-extra': "40px",
'@collapse-header-bg': "@background-color-light",
'@collapse-content-padding': "@padding-md",
'@collapse-content-bg': "@component-background",
'@skeleton-color': "#f2f2f2",
'@transfer-header-height': "40px",
'@transfer-disabled-bg': "@disabled-bg",
'@transfer-list-height': "200px",
'@message-notice-content-padding': "10px 16px",
'@wave-animation-width': "6px",
'@alert-success-border-color': "~`colorPalette('@{success-color}', 3) `",
'@alert-success-bg-color': "~`colorPalette('@{success-color}', 1) `",
'@alert-success-icon-color': "@success-color",
'@alert-info-border-color': "~`colorPalette('@{info-color}', 3) `",
'@alert-info-bg-color': "~`colorPalette('@{info-color}', 1) `",
'@alert-info-icon-color': "@info-color",
'@alert-warning-border-color': "~`colorPalette('@{warning-color}', 3) `",
'@alert-warning-bg-color': "~`colorPalette('@{warning-color}', 1) `",
'@alert-warning-icon-color': "@warning-color",
'@alert-error-border-color': "~`colorPalette('@{error-color}', 3) `",
'@alert-error-bg-color': "~`colorPalette('@{error-color}', 1) `",
'@alert-error-icon-color': "@error-color",
'@list-header-background': "transparent",
'@list-footer-background': "transparent",
'@list-empty-text-padding': "@padding-md",
'@list-item-padding': "@padding-sm 0",
'@list-item-meta-margin-bottom': "@padding-md",
'@list-item-meta-avatar-margin-right': "@padding-md",
'@list-item-meta-title-margin-bottom': "@padding-sm",
'@statistic-title-font-size': "@font-size-base",
'@statistic-content-font-size': "24px",
'@statistic-unit-font-size': "16px",
'@statistic-font-family': "@font-family",
'@drawer-header-padding': "16px 24px",
'@drawer-body-padding': "24px"
}
export default lightTheme

@ -0,0 +1,38 @@
module.exports = [
'@primary-color',
'@layout-header-background',
'@layout-sider-background',
'@layout-body-background',
'@background-color-base',
'@body-background',
'@layout-sider-background',
'@component-background',
'@layout-header-background',
'@input-bg',
'@btn-default-bg',
'@border-color-base',
'@border-color-split',
'@heading-color',
'@text-color',
'@text-color-secondary',
'@table-selected-row-bg',
'@table-expanded-row-bg',
'@table-header-bg',
'@table-row-hover-bg',
'@layout-trigger-color',
'@layout-trigger-background',
'@alert-message-color',
'@item-hover-bg',
'@item-active-bg',
'@disabled-color',
'@tag-default-bg',
'@popover-bg',
'@wait-icon-color',
'@background-color-light',
'@collapse-header-bg',
'@info-color',
'@primary-color',
'@highlight-color',
'@warning-color',
'@menu-dark-submenu-bg'
]

@ -0,0 +1,17 @@
@import "~antd/lib/style/themes/default.less";
@primary-color: #2196f3; // 全局主色
// @link-color: #1890ff; // 链接色
// @success-color: #52c41a; // 成功色
// @warning-color: #faad14; // 警告色
// @error-color: #f5222d; // 错误色
// @font-size-base: 14px; // 主字号
// @heading-color: rgba(0, 0, 0, 0.85); // 标题色
// @text-color: rgba(0, 0, 0, 0.65); // 主文本色
// @text-color-secondary : rgba(0, 0, 0, .45); // 次文本色
// @disabled-color : rgba(0, 0, 0, .25); // 失效色
// @border-radius-base: 4px; // 组件/浮层圆角
// @border-color-base: #d9d9d9; // 边框色
// @box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // 浮层阴影
@layout-header-background: #FFF;
@layout-sider-background: #FFF;

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"experimentalDecorators": true
},
"include": [
"src",
"typings/externals.d.ts"
]
}

@ -0,0 +1 @@
declare module '*.less'

File diff suppressed because it is too large Load Diff

14
ui_old/.gitignore vendored

@ -0,0 +1,14 @@
.DS_Store
node_modules/
/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln

@ -0,0 +1,21 @@
# vue-starter
> A vuejs starter project integrated with vuex, vue-router, less, iview
## Build Setup
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm run dev
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
```
For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

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

Loading…
Cancel
Save