primate: initial UI v0.1
Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
38
ui/.editorconfig
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
[*]
|
||||||
|
charset=utf-8
|
||||||
|
end_of_line=crlf
|
||||||
|
insert_final_newline=false
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[*.svg]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[*.js.map]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[*.less]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[*.vue]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[{.analysis_options,*.yml,*.yaml}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
1
ui/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
public/* linguist-vendored
|
||||||
21
ui/.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw*
|
||||||
5
ui/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
7
ui/.travis.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- 10.15.0
|
||||||
|
cache: yarn
|
||||||
|
script:
|
||||||
|
- yarn
|
||||||
|
- yarn run lint --no-fix && yarn run build
|
||||||
97
ui/README.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# CloudStack Primate
|
||||||
|
|
||||||
|
A progressive modern CloudStack Admin UI based on VueJS and Ant Design.
|
||||||
|
|
||||||
|
Install tools and dependencies:
|
||||||
|
|
||||||
|
sudo apt-get install npm
|
||||||
|
sudo npm i -g npm@next
|
||||||
|
sudo npm i -g npm-check-updates
|
||||||
|
ncu -u # optional: upgrade dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
Build and run:
|
||||||
|
|
||||||
|
npm run serve
|
||||||
|
|
||||||
|
Production Build:
|
||||||
|
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
Upgrade dependencies:
|
||||||
|
|
||||||
|
|
||||||
|
Run Tests:
|
||||||
|
|
||||||
|
npm run test
|
||||||
|
npm run lint
|
||||||
|
npm run test:unit
|
||||||
|
|
||||||
|
Fix issues and vulnerabilities:
|
||||||
|
|
||||||
|
npm audit
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
The project was created by Rohit Yadav over several weekends during late 2018.
|
||||||
|
The base app layout was referenced from [Ant Design Pro
|
||||||
|
Vue](https://github.com/sendya/ant-design-pro-vue).
|
||||||
|
|
||||||
|
### Env and dependencies
|
||||||
|
|
||||||
|
- node
|
||||||
|
- webpack
|
||||||
|
- eslint
|
||||||
|
- @vue/cli ~3
|
||||||
|
- [ant-design-vue](https://github.com/vueComponent/ant-design-vue) - Ant Design Of Vue
|
||||||
|
- [vue-cropper](https://github.com/xyxiao001/vue-cropper) - Picture edit
|
||||||
|
- [@antv/g2](https://antv.alipay.com/zh-cn/index.html) - AntV G2
|
||||||
|
- [Viser-vue](https://viserjs.github.io/docs.html#/viser/guide/installation) - Antv/G2 of Vue
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- [Vue-cli3](https://cli.vuejs.org/guide/) used by the project.
|
||||||
|
- Disable Eslint (not recommended): remove `eslintConfig` field in `package.json`
|
||||||
|
|
||||||
|
- Easy-Mock used by project,[easy-mock](https://www.easy-mock.com/) Project API Data [DO NOT CHANGE THE INTERFACE](https://www.easy-mock.com/project/5b7bce071f130e5b7fe8cd7d),If you want to modify, please fork [ANTD-PRO-Easy-Mock-API.zip](https://github.com/sendya/ant-design-pro-vue/files/2682711/ANTD-PRO-Easy-Mock-API.zip) and running to your server.
|
||||||
|
|
||||||
|
- Load on Demand: modify `/src/main.js` L7, append `import './core/lazy_use'` code.
|
||||||
|
|
||||||
|
- Customize Theme: `vue.config.js`
|
||||||
|
eg:
|
||||||
|
```ecmascript 6
|
||||||
|
css: {
|
||||||
|
loaderOptions: {
|
||||||
|
less: {
|
||||||
|
modifyVars: {
|
||||||
|
/* Less variables, required modifyVars*/
|
||||||
|
|
||||||
|
'primary-color': '#F5222D',
|
||||||
|
'link-color': '#F5222D',
|
||||||
|
'border-radius-base': '4px',
|
||||||
|
},
|
||||||
|
javascriptEnabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
- [Router and Menu](https://github.com/sendya/ant-design-pro-vue/blob/master/src/router/README.md)
|
||||||
|
- [Table](https://github.com/sendya/ant-design-pro-vue/blob/master/src/components/table/README.md) [@Saraka](https://github.com/saraka-tsukai)
|
||||||
|
- [ANTD DefaultConfig](https://github.com/sendya/ant-design-pro-vue/blob/master/src/defaultSettings.js)
|
||||||
|
- [Frist page loading animate](https://github.com/sendya/ant-design-pro-vue/blob/master/docs/add-page-loading-animate.md)
|
||||||
|
- [Multi-Tabs feature/multi-tabs](https://github.com/sendya/ant-design-pro-vue/tree/feature/multi-tabs) [How to remove](https://github.com/sendya/ant-design-pro-vue/blob/master/docs/multi-tabs.md)
|
||||||
|
- [LoadOnDemand Demo feature/demand_load](https://github.com/sendya/ant-design-pro-vue/tree/feature/demand_load)
|
||||||
|
- [LoadOnDemand Docs](https://github.com/sendya/ant-design-pro-vue/blob/master/docs/load-on-demand.md)
|
||||||
|
- [i18n feature/lang](https://github.com/sendya/ant-design-pro-vue/tree/feature/lang) Creator [@musnow](https://github.com/musnow)
|
||||||
|
- [Dependency analysis tool: analyzer](https://github.com/sendya/ant-design-pro-vue/blob/master/docs/webpack-bundle-analyzer.md)
|
||||||
|
- ANTD PRO Components:
|
||||||
|
- Trend [Trend.md](https://github.com/sendya/ant-design-pro-vue/blob/master/src/components/Trend/index.md)
|
||||||
|
- AvatarList [AvatarList.md](https://github.com/sendya/ant-design-pro-vue/blob/master/src/components/AvatarList/index.md)
|
||||||
|
- CountDown [CountDown.md](https://github.com/sendya/ant-design-pro-vue/blob/master/src/components/CountDown/index.md)
|
||||||
|
- Ellipsis [Ellipsis.md](https://github.com/sendya/ant-design-pro-vue/blob/master/src/components/Ellipsis/index.md)
|
||||||
|
- NumberInfo [NumberInfo.md](https://github.com/sendya/ant-design-pro-vue/blob/master/src/components/NumberInfo/index.md)
|
||||||
|
- FooterToolbar [FooterToolbar.md](https://github.com/sendya/ant-design-pro-vue/blob/master/src/components/FooterToolbar/index.md)
|
||||||
|
- IconSelector [IconSelector.md](https://github.com/sendya/ant-design-pro-vue/blob/master/src/components/IconSelector/README.md) Creator: [@Saraka](https://github.com/saraka-tsukai)
|
||||||
14
ui/babel.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/app'
|
||||||
|
]
|
||||||
|
// if your use import on Demand, Use this code
|
||||||
|
// ,
|
||||||
|
// plugins: [
|
||||||
|
// [ 'import', {
|
||||||
|
// 'libraryName': 'ant-design-vue',
|
||||||
|
// 'libraryDirectory': 'es',
|
||||||
|
// 'style': true
|
||||||
|
// } ]
|
||||||
|
// ]
|
||||||
|
}
|
||||||
23
ui/jest.config.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
module.exports = {
|
||||||
|
moduleFileExtensions: [
|
||||||
|
'js',
|
||||||
|
'jsx',
|
||||||
|
'json',
|
||||||
|
'vue'
|
||||||
|
],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.vue$': 'vue-jest',
|
||||||
|
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
|
||||||
|
'^.+\\.jsx?$': 'babel-jest'
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^@/(.*)$': '<rootDir>/src/$1'
|
||||||
|
},
|
||||||
|
snapshotSerializers: [
|
||||||
|
'jest-serializer-vue'
|
||||||
|
],
|
||||||
|
testMatch: [
|
||||||
|
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
|
||||||
|
],
|
||||||
|
testURL: 'http://localhost/'
|
||||||
|
}
|
||||||
11
ui/jsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
18494
ui/package-lock.json
generated
Normal file
123
ui/package.json
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"name": "primate",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint",
|
||||||
|
"test:unit": "vue-cli-service test:unit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@antv/data-set": "^0.10.2",
|
||||||
|
"ant-design-vue": "~1.3.8",
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"enquire.js": "^2.1.6",
|
||||||
|
"js-cookie": "^2.2.0",
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.pick": "^4.4.0",
|
||||||
|
"md5": "^2.2.1",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"viser-vue": "^2.4.6",
|
||||||
|
"vue": "^2.6.10",
|
||||||
|
"vue-clipboard2": "^0.3.0",
|
||||||
|
"vue-cookies": "^1.5.13",
|
||||||
|
"vue-cropper": "0.4.9",
|
||||||
|
"vue-ls": "^3.2.1",
|
||||||
|
"vue-router": "^3.0.6",
|
||||||
|
"vue-svg-component-runtime": "^1.0.1",
|
||||||
|
"vuex": "^3.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/polyfill": "^7.4.4",
|
||||||
|
"@vue/cli-plugin-babel": "^3.7.0",
|
||||||
|
"@vue/cli-plugin-eslint": "^3.7.0",
|
||||||
|
"@vue/cli-plugin-unit-jest": "^3.7.0",
|
||||||
|
"@vue/cli-service": "^3.7.0",
|
||||||
|
"@vue/eslint-config-standard": "^4.0.0",
|
||||||
|
"@vue/test-utils": "^1.0.0-beta.20",
|
||||||
|
"babel-core": "7.0.0-bridge.0",
|
||||||
|
"babel-eslint": "^10.0.1",
|
||||||
|
"babel-jest": "^24.8.0",
|
||||||
|
"babel-plugin-import": "^1.11.0",
|
||||||
|
"eslint": "^5.16.0",
|
||||||
|
"eslint-plugin-html": "^5.0.3",
|
||||||
|
"eslint-plugin-vue": "^5.2.2",
|
||||||
|
"less": "^3.9.0",
|
||||||
|
"less-loader": "^5.0.0",
|
||||||
|
"vue-svg-icon-loader": "^2.1.1",
|
||||||
|
"vue-template-compiler": "^2.6.10"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/strongly-recommended",
|
||||||
|
"@vue/standard"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"generator-star-spacing": "off",
|
||||||
|
"no-mixed-operators": 0,
|
||||||
|
"vue/max-attributes-per-line": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"singleline": 5,
|
||||||
|
"multiline": {
|
||||||
|
"max": 1,
|
||||||
|
"allowFirstLine": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vue/attribute-hyphenation": 0,
|
||||||
|
"vue/html-self-closing": 0,
|
||||||
|
"vue/component-name-in-template-casing": 0,
|
||||||
|
"vue/html-closing-bracket-spacing": 0,
|
||||||
|
"vue/singleline-html-element-content-newline": 0,
|
||||||
|
"vue/no-unused-components": 0,
|
||||||
|
"vue/multiline-html-element-content-newline": 0,
|
||||||
|
"vue/no-use-v-if-with-v-for": 0,
|
||||||
|
"vue/html-closing-bracket-newline": 0,
|
||||||
|
"vue/no-parsing-error": 0,
|
||||||
|
"no-console": 0,
|
||||||
|
"no-tabs": 0,
|
||||||
|
"quotes": [
|
||||||
|
2,
|
||||||
|
"single",
|
||||||
|
{
|
||||||
|
"avoidEscape": true,
|
||||||
|
"allowTemplateLiterals": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
2,
|
||||||
|
"never",
|
||||||
|
{
|
||||||
|
"beforeStatementContinuationChars": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-delete-var": 2,
|
||||||
|
"prefer-const": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"ignoreReadBeforeAssign": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postcss": {
|
||||||
|
"plugins": {
|
||||||
|
"autoprefixer": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not ie <= 10"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
ui/public/cloud.ico
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
7684
ui/public/color.less
vendored
Normal file
19
ui/public/index.html
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-gb">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>cloud.ico">
|
||||||
|
<title>Apache CloudStack</title>
|
||||||
|
<style>#preloadingAnimation{position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;z-index: 9999;overflow: hidden}.lds-roller{display:inline-block;position:relative;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;}.lds-roller div{animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;transform-origin:32px 32px;}.lds-roller div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#39a7de;margin:-3px 0 0 -3px;}.lds-roller div:nth-child(1){animation-delay:-0.036s;}.lds-roller div:nth-child(1):after{top:50px;left:50px;}.lds-roller div:nth-child(2){animation-delay:-0.072s;}.lds-roller div:nth-child(2):after{top:54px;left:45px;}.lds-roller div:nth-child(3){animation-delay:-0.108s;}.lds-roller div:nth-child(3):after{top:57px;left:39px;}.lds-roller div:nth-child(4){animation-delay:-0.144s;}.lds-roller div:nth-child(4):after{top:58px;left:32px;}.lds-roller div:nth-child(5){animation-delay:-0.18s;}.lds-roller div:nth-child(5):after{top:57px;left:25px;}.lds-roller div:nth-child(6){animation-delay:-0.216s;}.lds-roller div:nth-child(6):after{top:54px;left:19px;}.lds-roller div:nth-child(7){animation-delay:-0.252s;}.lds-roller div:nth-child(7):after{top:50px;left:14px;}.lds-roller div:nth-child(8){animation-delay:-0.288s;}.lds-roller div:nth-child(8):after{top:45px;left:10px;}#preloadingAnimation .load-tips{color: #39a7de;font-size:2rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin-top:80px;text-align:center;width:400px;height:64px;} @keyframes lds-roller{0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);}}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but CloudStack Primate UI doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app">
|
||||||
|
<div id="preloadingAnimation"><div class=lds-roller><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><div class=load-tips>Loading</div></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
ui/public/loading/loading.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
#preloadingAnimation{position:fixed;left:0;top:0;height:100%;width:100%;background:#ffffff;user-select:none;z-index: 9999;overflow: hidden}.lds-roller{display:inline-block;position:relative;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;}.lds-roller div{animation:lds-roller 1.2s cubic-bezier(0.5,0,0.5,1) infinite;transform-origin:32px 32px;}.lds-roller div:after{content:" ";display:block;position:absolute;width:6px;height:6px;border-radius:50%;background:#13c2c2;margin:-3px 0 0 -3px;}.lds-roller div:nth-child(1){animation-delay:-0.036s;}.lds-roller div:nth-child(1):after{top:50px;left:50px;}.lds-roller div:nth-child(2){animation-delay:-0.072s;}.lds-roller div:nth-child(2):after{top:54px;left:45px;}.lds-roller div:nth-child(3){animation-delay:-0.108s;}.lds-roller div:nth-child(3):after{top:57px;left:39px;}.lds-roller div:nth-child(4){animation-delay:-0.144s;}.lds-roller div:nth-child(4):after{top:58px;left:32px;}.lds-roller div:nth-child(5){animation-delay:-0.18s;}.lds-roller div:nth-child(5):after{top:57px;left:25px;}.lds-roller div:nth-child(6){animation-delay:-0.216s;}.lds-roller div:nth-child(6):after{top:54px;left:19px;}.lds-roller div:nth-child(7){animation-delay:-0.252s;}.lds-roller div:nth-child(7):after{top:50px;left:14px;}.lds-roller div:nth-child(8){animation-delay:-0.288s;}.lds-roller div:nth-child(8):after{top:45px;left:10px;}#preloadingAnimation .load-tips{color: #13c2c2;font-size:2rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin-top:80px;text-align:center;width:400px;height:64px;} @keyframes lds-roller{0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);}}
|
||||||
1
ui/public/loading/loading.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div id="preloadingAnimation"><div class=lds-roller><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><div class=load-tips>Loading</div></div>
|
||||||
5
ui/public/loading/option2/html_code_segment.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<div class="preloading-animate">
|
||||||
|
<div class="preloading-wrapper">
|
||||||
|
<svg class="preloading-balls" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><circle cx="67.802" cy="59.907" r="6" fill="#51CACC"><animate attributeName="cx" values="75;57.72542485937369" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="50;73.77641290737884" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#51CACC;#9DF871" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="46.079" cy="69.992" r="6" fill="#9DF871"><animate attributeName="cx" values="57.72542485937369;29.774575140626318" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="73.77641290737884;64.69463130731182" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#9DF871;#E0FF77" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="29.775" cy="52.449" r="6" fill="#E0FF77"><animate attributeName="cx" values="29.774575140626318;29.774575140626315" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="64.69463130731182;35.30536869268818" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#E0FF77;#DE9DD6" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="41.421" cy="31.521" r="6" fill="#DE9DD6"><animate attributeName="cx" values="29.774575140626315;57.72542485937368" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="35.30536869268818;26.22358709262116" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#DE9DD6;#FF708E" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="64.923" cy="36.13" r="6" fill="#FF708E"><animate attributeName="cx" values="57.72542485937368;75" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="26.22358709262116;49.99999999999999" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#FF708E;#51CACC" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
1
ui/public/loading/option2/loading.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
.preloading-animate{background:#ffffff;width:100%;height:100%;position:fixed;left:0;top:0;z-index:299;}.preloading-animate .preloading-wrapper{position:absolute;width:5rem;height:5rem;left:50%;top:50%;transform:translate(-50%,-50%);}.preloading-animate .preloading-wrapper .preloading-balls{font-size:5rem;}
|
||||||
1
ui/public/loading/option2/loading.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg class="preloading-balls" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><circle cx="67.802" cy="59.907" r="6" fill="#51CACC"><animate attributeName="cx" values="75;57.72542485937369" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="50;73.77641290737884" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#51CACC;#9DF871" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="46.079" cy="69.992" r="6" fill="#9DF871"><animate attributeName="cx" values="57.72542485937369;29.774575140626318" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="73.77641290737884;64.69463130731182" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#9DF871;#E0FF77" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="29.775" cy="52.449" r="6" fill="#E0FF77"><animate attributeName="cx" values="29.774575140626318;29.774575140626315" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="64.69463130731182;35.30536869268818" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#E0FF77;#DE9DD6" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="41.421" cy="31.521" r="6" fill="#DE9DD6"><animate attributeName="cx" values="29.774575140626315;57.72542485937368" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="35.30536869268818;26.22358709262116" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#DE9DD6;#FF708E" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle><circle cx="64.923" cy="36.13" r="6" fill="#FF708E"><animate attributeName="cx" values="57.72542485937368;75" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="cy" values="26.22358709262116;49.99999999999999" keyTimes="0;1" dur="1s" repeatCount="indefinite"/><animate attributeName="fill" values="#FF708E;#51CACC" keyTimes="0;1" dur="1s" repeatCount="indefinite"/></circle></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
29
ui/src/App.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<a-locale-provider :locale="locale">
|
||||||
|
<div id="app">
|
||||||
|
<router-view/>
|
||||||
|
</div>
|
||||||
|
</a-locale-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import enUS from 'ant-design-vue/lib/locale-provider/en_US'
|
||||||
|
import { AppDeviceEnquire } from '@/utils/mixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [AppDeviceEnquire],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
locale: enUS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
ui/src/api/api.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { axios } from '@/utils/request'
|
||||||
|
|
||||||
|
export function api (command, args = {}) {
|
||||||
|
const params = {
|
||||||
|
command: command,
|
||||||
|
response: 'json'
|
||||||
|
}
|
||||||
|
for (var arg in args) {
|
||||||
|
params[arg] = args
|
||||||
|
}
|
||||||
|
console.log(params)
|
||||||
|
return axios.get('/', {
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login (arg) {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('command', 'login')
|
||||||
|
params.append('username', arg.username)
|
||||||
|
params.append('password', arg.password)
|
||||||
|
params.append('domain', arg.domain)
|
||||||
|
params.append('response', 'json')
|
||||||
|
return axios({
|
||||||
|
url: '/',
|
||||||
|
method: 'post',
|
||||||
|
data: params,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout () {
|
||||||
|
return api('logout')
|
||||||
|
}
|
||||||
30
ui/src/api/index.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { axios } from '@/utils/request'
|
||||||
|
|
||||||
|
export function api (command, args = {}) {
|
||||||
|
args['command'] = command
|
||||||
|
args['response'] = 'json'
|
||||||
|
return axios.get('/', {
|
||||||
|
params: args
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login (arg) {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('command', 'login')
|
||||||
|
params.append('username', arg.username)
|
||||||
|
params.append('password', arg.password)
|
||||||
|
params.append('domain', arg.domain)
|
||||||
|
params.append('response', 'json')
|
||||||
|
return axios({
|
||||||
|
url: '/',
|
||||||
|
method: 'post',
|
||||||
|
data: params,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout () {
|
||||||
|
return api('logout')
|
||||||
|
}
|
||||||
69
ui/src/assets/background.svg
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>Group 21</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
|
||||||
|
<g id="Group-21" transform="translate(77.000000, 73.000000)">
|
||||||
|
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
|
||||||
|
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
|
||||||
|
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
|
||||||
|
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
|
||||||
|
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||||
|
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||||
|
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
|
||||||
|
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
|
||||||
|
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
|
||||||
|
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
|
||||||
|
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
|
||||||
|
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
|
||||||
|
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
|
||||||
|
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
|
||||||
|
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
|
||||||
|
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
|
||||||
|
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
|
||||||
|
</g>
|
||||||
|
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||||
|
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||||
|
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
|
||||||
|
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
|
||||||
|
</g>
|
||||||
|
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
|
||||||
|
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
|
||||||
|
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
|
||||||
|
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
|
||||||
|
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
|
||||||
|
</g>
|
||||||
|
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||||
|
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
|
||||||
|
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
|
||||||
|
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
|
||||||
|
</g>
|
||||||
|
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
|
||||||
|
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
|
||||||
|
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
|
||||||
|
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
|
||||||
|
</g>
|
||||||
|
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
|
||||||
|
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||||
|
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||||
|
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
|
||||||
|
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
|
||||||
|
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||||
|
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
|
||||||
|
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
|
||||||
|
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
|
||||||
|
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
|
||||||
|
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
|
||||||
|
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.7 KiB |
BIN
ui/src/assets/banner.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
ui/src/assets/cloudmonkey.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
0
ui/src/assets/cloudmonkey.svg
Normal file
1
ui/src/assets/icons/bx-analyse.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1551058675966" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7872" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M85.333333 512h85.333334a340.736 340.736 0 0 1 99.712-241.621333 337.493333 337.493333 0 0 1 108.458666-72.96 346.453333 346.453333 0 0 1 261.546667-1.749334A106.154667 106.154667 0 0 0 746.666667 298.666667C805.802667 298.666667 853.333333 251.136 853.333333 192S805.802667 85.333333 746.666667 85.333333c-29.397333 0-55.978667 11.776-75.221334 30.933334-103.722667-41.514667-222.848-40.874667-325.76 2.517333a423.594667 423.594667 0 0 0-135.68 91.264 423.253333 423.253333 0 0 0-91.306666 135.637333A426.88 426.88 0 0 0 85.333333 512z m741.248 133.205333c-17.109333 40.618667-41.685333 77.141333-72.96 108.416s-67.797333 55.850667-108.458666 72.96a346.453333 346.453333 0 0 1-261.546667 1.749334A106.154667 106.154667 0 0 0 277.333333 725.333333C218.197333 725.333333 170.666667 772.864 170.666667 832S218.197333 938.666667 277.333333 938.666667c29.397333 0 55.978667-11.776 75.221334-30.933334A425.173333 425.173333 0 0 0 512 938.666667a425.941333 425.941333 0 0 0 393.258667-260.352A426.325333 426.325333 0 0 0 938.666667 512h-85.333334a341.034667 341.034667 0 0 1-26.752 133.205333z" p-id="7873"></path><path d="M512 318.378667c-106.752 0-193.621333 86.869333-193.621333 193.621333S405.248 705.621333 512 705.621333s193.621333-86.869333 193.621333-193.621333S618.752 318.378667 512 318.378667z m0 301.909333c-59.690667 0-108.288-48.597333-108.288-108.288S452.309333 403.712 512 403.712s108.288 48.597333 108.288 108.288-48.597333 108.288-108.288 108.288z" p-id="7874"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
ui/src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
405
ui/src/components/CloudMonkey/Resource.vue
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div style="margin-bottom: 16px; margin-top: 16px">
|
||||||
|
<a-button
|
||||||
|
@click="fetchData"
|
||||||
|
:loading="loading"
|
||||||
|
shape="circle"
|
||||||
|
icon="reload"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-button
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index"
|
||||||
|
:icon="action.icon"
|
||||||
|
:type="action.icon == 'delete' ? 'danger' : (action.icon == 'plus' ? 'primary' : 'default')"
|
||||||
|
shape="circle"
|
||||||
|
style="margin-left: 5px"
|
||||||
|
@click="execAction(action)"
|
||||||
|
>
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<a-drawer
|
||||||
|
:title="action.label"
|
||||||
|
placement="right"
|
||||||
|
width="75%"
|
||||||
|
:closable="true"
|
||||||
|
@close="closeAction"
|
||||||
|
:visible="showAction"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="action.loading">
|
||||||
|
<a-form
|
||||||
|
:form="form"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
layout="vertical" >
|
||||||
|
<a-form-item
|
||||||
|
v-for="(field, index) in action.params"
|
||||||
|
:key="index"
|
||||||
|
:label="field.name"
|
||||||
|
:v-bind="field.name">
|
||||||
|
|
||||||
|
<span v-if="field.type==='boolean'">
|
||||||
|
<a-switch
|
||||||
|
v-decorator="[field.name, {
|
||||||
|
rules: [{ required: field.required, message: 'Please provide input' }]
|
||||||
|
}]"
|
||||||
|
:placeholder="field.description"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="field.type==='uuid' || field.name==='account'">
|
||||||
|
<a-select
|
||||||
|
:loading="field.loading"
|
||||||
|
v-decorator="[field.name, {
|
||||||
|
rules: [{ required: field.required, message: 'Please select option' }]
|
||||||
|
}]"
|
||||||
|
:placeholder="field.description"
|
||||||
|
|
||||||
|
>
|
||||||
|
<a-select-option v-for="(opt, index) in field.opts" :key="index">
|
||||||
|
{{ opt.name }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="field.type==='long'">
|
||||||
|
<a-input-number
|
||||||
|
v-decorator="[field.name, {
|
||||||
|
rules: [{ required: field.required, message: 'Please enter a number' }]
|
||||||
|
}]"
|
||||||
|
:placeholder="field.description"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<a-input
|
||||||
|
v-decorator="[field.name, {
|
||||||
|
rules: [{ required: field.required, message: 'Please enter input' }]
|
||||||
|
}]"
|
||||||
|
:placeholder="field.description"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
borderTop: '1px solid #e8e8e8',
|
||||||
|
paddingTop: '24px',
|
||||||
|
textAlign: 'right',
|
||||||
|
left: 0,
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: '0 0 4px 4px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
style="marginRight: 8px"
|
||||||
|
@click="closeAction"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
:loading="action.loading"
|
||||||
|
type="primary"
|
||||||
|
html-type="submit">
|
||||||
|
Submit
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-spin>
|
||||||
|
</a-drawer>
|
||||||
|
|
||||||
|
<span style="margin-left: 8px">
|
||||||
|
<template v-if="hasSelected">
|
||||||
|
{{ `Selected ${selectedRowKeys.length} items` }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$route.params && $route.params.id">
|
||||||
|
<p v-for="(value, key) in getResource($route.params.id)" :key="key">
|
||||||
|
<span>{{ key }}: </span>
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<a-table
|
||||||
|
size="middle"
|
||||||
|
:rowKey="record => record.id"
|
||||||
|
:loading="loading"
|
||||||
|
:columns="columns"
|
||||||
|
:dataSource="items"
|
||||||
|
:scroll="{ x: true }"
|
||||||
|
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
|
||||||
|
>
|
||||||
|
<a slot="name" slot-scope="text, record" href="javascript:;">
|
||||||
|
<router-link :to="{ path: $route.name + '/' + record.id }">{{ text }}</router-link>
|
||||||
|
</a>
|
||||||
|
<a slot="username" slot-scope="text, record" href="javascript:;">
|
||||||
|
<router-link :to="{ path: $route.name + '/' + record.id }">{{ text }}</router-link>
|
||||||
|
</a>
|
||||||
|
<a slot="vmname" slot-scope="text, record" href="javascript:;">
|
||||||
|
<router-link :to="{ path: '/vm/' + record.virtualmachineid }">{{ text }}</router-link>
|
||||||
|
</a>
|
||||||
|
<a slot="account" slot-scope="text, record" href="javascript:;">
|
||||||
|
<router-link :to="{ path: '/account/' + record.accountid }">{{ text }}</router-link>
|
||||||
|
</a>
|
||||||
|
<a slot="domain" slot-scope="text, record" href="javascript:;">
|
||||||
|
<router-link :to="{ path: '/domain/' + record.domainid }">{{ text }}</router-link>
|
||||||
|
</a>
|
||||||
|
<a slot="zonename" slot-scope="text, record" href="javascript:;">
|
||||||
|
<router-link :to="{ path: '/zone/' + record.zoneid }">{{ text }}</router-link>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a slot="guestnetworkname" slot-scope="text, record" href="javascript:;">
|
||||||
|
<router-link :to="{ path: '/guestnetwork/' + record.guestnetworkid }">{{ text }}</router-link>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from '@/api'
|
||||||
|
import { apiConfig } from '@/config/apiConfig'
|
||||||
|
import store from '@/store'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Resource',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
apiName: '',
|
||||||
|
config: {},
|
||||||
|
loading: false,
|
||||||
|
columns: [],
|
||||||
|
items: [],
|
||||||
|
selectedRowKeys: [],
|
||||||
|
action: {},
|
||||||
|
showAction: false,
|
||||||
|
actions: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasSelected () {
|
||||||
|
return this.selectedRowKeys.length > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route' (to, from) {
|
||||||
|
if (to.name === this.$route.name) {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeCreate () {
|
||||||
|
this.form = this.$form.createForm(this)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
this.apiName = ''
|
||||||
|
this.actions = []
|
||||||
|
this.columns = []
|
||||||
|
this.columnKeys = []
|
||||||
|
this.config = apiConfig[this.$route.name]
|
||||||
|
var params = { listall: true }
|
||||||
|
if (this.$route.meta.params) {
|
||||||
|
params = this.$route.meta.params
|
||||||
|
params['listall'] = 'true'
|
||||||
|
}
|
||||||
|
if (this.config) {
|
||||||
|
this.apiName = this.config.listApi
|
||||||
|
this.actions = this.config.actions
|
||||||
|
this.columnKeys = this.config.column
|
||||||
|
} else {
|
||||||
|
if (this.$route && this.$route.meta && this.$route.meta.permission) {
|
||||||
|
this.apiName = this.$route.meta.permission[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.apiName && this.apiName !== '' && !this.columnKeys || this.columnKeys.length == 0) {
|
||||||
|
for (const field of store.getters.apis[this.apiName]['response']) {
|
||||||
|
this.columnKeys.push(field.name)
|
||||||
|
}
|
||||||
|
this.columnKeys = [...new Set(this.columnKeys)]
|
||||||
|
this.columnKeys.sort(function (a, b) {
|
||||||
|
if (a === 'name' && b !== 'name') { return -1 }
|
||||||
|
if (a < b) { return -1 }
|
||||||
|
if (a > b) { return 1 }
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const key of this.columnKeys) {
|
||||||
|
this.columns.push({
|
||||||
|
title: key,
|
||||||
|
dataIndex: key,
|
||||||
|
key: key,
|
||||||
|
scopedSlots: { customRender: key },
|
||||||
|
sorter: (a, b) => a[key].length - b[key].length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
if (this.$route.params && this.$route.params.id) {
|
||||||
|
params['id'] = this.$route.params.id
|
||||||
|
}
|
||||||
|
api(this.apiName, params).then(json => {
|
||||||
|
this.loading = false
|
||||||
|
var responseName
|
||||||
|
var objectName
|
||||||
|
for (const key in json) {
|
||||||
|
if (key.includes('response')) {
|
||||||
|
responseName = key
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key in json[responseName]) {
|
||||||
|
if (key == 'count') continue
|
||||||
|
objectName = key
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.items = json[responseName][objectName]
|
||||||
|
if (!this.items || this.items.length == 0) {
|
||||||
|
this.items = []
|
||||||
|
}
|
||||||
|
for (let idx = 0; idx < this.items.length; idx++) {
|
||||||
|
this.items[idx]['key'] = idx
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getResource (id) {
|
||||||
|
var res = {}
|
||||||
|
for (const item of this.items) {
|
||||||
|
if (item.id == id) {
|
||||||
|
res = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
closeAction () {
|
||||||
|
this.action.loading = false
|
||||||
|
this.showAction = false
|
||||||
|
this.action = {}
|
||||||
|
},
|
||||||
|
execAction (action) {
|
||||||
|
this.action = action
|
||||||
|
this.action['params'] = store.getters.apis[action.api]['params']
|
||||||
|
this.action['params'].sort(function (a, b) {
|
||||||
|
if (a.name === 'name' && b.name !== 'name') { return -1 }
|
||||||
|
if (a.name !== 'name' && b.name === 'name') { return -1 }
|
||||||
|
if (a.name === 'id') { return -1 }
|
||||||
|
if (a.name < b.name) { return -1 }
|
||||||
|
if (a.name > b.name) { return 1 }
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
for (var param of this.action['params']) {
|
||||||
|
if (param.type === 'uuid' || param.name === 'account') {
|
||||||
|
this.listUuidOpts(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.showAction = true
|
||||||
|
this.action.loading = false
|
||||||
|
},
|
||||||
|
listUuidOpts (param) {
|
||||||
|
var paramName = param.name
|
||||||
|
const possibleName = 'list' + paramName.replace('id', '').toLowerCase() + 's'
|
||||||
|
var possibleApi = undefined
|
||||||
|
if (paramName == 'id') {
|
||||||
|
possibleApi = this.apiName
|
||||||
|
} else {
|
||||||
|
for (const api in store.getters.apis) {
|
||||||
|
if (api.toLowerCase().startsWith(possibleName)) {
|
||||||
|
possibleApi = api
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!possibleApi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
param.loading = true
|
||||||
|
param.opts = []
|
||||||
|
var params = { listall: true }
|
||||||
|
if (possibleApi == 'listTemplates') {
|
||||||
|
params['templatefilter'] = 'executable'
|
||||||
|
}
|
||||||
|
api(possibleApi, params).then(json => {
|
||||||
|
param.loading = false
|
||||||
|
for (const obj in json) {
|
||||||
|
if (obj.includes('response')) {
|
||||||
|
for (const res in json[obj]) {
|
||||||
|
if (res == 'count') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
param.opts = json[obj][res]
|
||||||
|
this.$forceUpdate()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(function (error) {
|
||||||
|
param.loading = false
|
||||||
|
}).then(function () {
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleSubmit (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
this.action.loading = true
|
||||||
|
const params = {}
|
||||||
|
for (const key in values) {
|
||||||
|
const input = values[key]
|
||||||
|
for (const param of this.action['params']) {
|
||||||
|
if (param.name === key) {
|
||||||
|
if (input === undefined) {
|
||||||
|
if (param.type === 'boolean') {
|
||||||
|
params[key] = false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (param.type === 'uuid') {
|
||||||
|
params[key] = param.opts[input]['id']
|
||||||
|
} else {
|
||||||
|
params[key] = input
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api(this.action.api, params).then(json => {
|
||||||
|
console.log(json)
|
||||||
|
this.closeAction()
|
||||||
|
}).catch(function (error) {
|
||||||
|
console.log(error)
|
||||||
|
this.closeAction()
|
||||||
|
}).then(function () {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
start () {
|
||||||
|
this.loading = true
|
||||||
|
this.fetchData()
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loading = false
|
||||||
|
this.selectedRowKeys = []
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
onSelectChange (selectedRowKeys) {
|
||||||
|
console.log('selectedRowKeys changed: ', selectedRowKeys)
|
||||||
|
this.selectedRowKeys = selectedRowKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
102
ui/src/components/CountDown/CountDown.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
{{ lastTime | format }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function fixedZero (val) {
|
||||||
|
return val * 1 < 10 ? `0${val}` : val
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CountDown',
|
||||||
|
props: {
|
||||||
|
format: {
|
||||||
|
type: Function,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
type: [Date, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
onEnd: {
|
||||||
|
type: Function,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
dateTime: '0',
|
||||||
|
originTargetTime: 0,
|
||||||
|
lastTime: 0,
|
||||||
|
timer: 0,
|
||||||
|
interval: 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
format (time) {
|
||||||
|
const hours = 60 * 60 * 1000
|
||||||
|
const minutes = 60 * 1000
|
||||||
|
|
||||||
|
const h = Math.floor(time / hours)
|
||||||
|
const m = Math.floor((time - h * hours) / minutes)
|
||||||
|
const s = Math.floor((time - h * hours - m * minutes) / 1000)
|
||||||
|
return `${fixedZero(h)}:${fixedZero(m)}:${fixedZero(s)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.initTime()
|
||||||
|
this.tick()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initTime () {
|
||||||
|
let lastTime = 0
|
||||||
|
let targetTime = 0
|
||||||
|
this.originTargetTime = this.target
|
||||||
|
try {
|
||||||
|
if (Object.prototype.toString.call(this.target) === '[object Date]') {
|
||||||
|
targetTime = this.target
|
||||||
|
} else {
|
||||||
|
targetTime = new Date(this.target).getTime()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('invalid target prop')
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTime = targetTime - new Date().getTime()
|
||||||
|
|
||||||
|
this.lastTime = lastTime < 0 ? 0 : lastTime
|
||||||
|
},
|
||||||
|
tick () {
|
||||||
|
const { onEnd } = this
|
||||||
|
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
if (this.lastTime < this.interval) {
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
this.lastTime = 0
|
||||||
|
if (typeof onEnd === 'function') {
|
||||||
|
onEnd()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.lastTime -= this.interval
|
||||||
|
this.tick()
|
||||||
|
}
|
||||||
|
}, this.interval)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUpdate () {
|
||||||
|
if (this.originTargetTime !== this.target) {
|
||||||
|
this.initTime()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
3
ui/src/components/CountDown/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import CountDown from './CountDown'
|
||||||
|
|
||||||
|
export default CountDown
|
||||||
64
ui/src/components/Ellipsis/Ellipsis.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<script>
|
||||||
|
import Tooltip from 'ant-design-vue/es/tooltip'
|
||||||
|
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
|
||||||
|
/*
|
||||||
|
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
|
||||||
|
|
||||||
|
const TooltipOverlayStyle = {
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Ellipsis',
|
||||||
|
components: {
|
||||||
|
Tooltip
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
prefixCls: {
|
||||||
|
type: String,
|
||||||
|
default: 'ant-pro-ellipsis'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lines: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
fullWidthRecognition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getStrDom (str, fullLength) {
|
||||||
|
return (
|
||||||
|
<span>{ cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '') }</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
getTooltip (fullStr, fullLength) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<template slot="title">{ fullStr }</template>
|
||||||
|
{ this.getStrDom(fullStr, fullLength) }
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render () {
|
||||||
|
const { tooltip, length } = this.$props
|
||||||
|
const str = this.$slots.default.map(vNode => vNode.text).join('')
|
||||||
|
const fullLength = getStrFullLength(str)
|
||||||
|
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
|
||||||
|
return (
|
||||||
|
strDom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
3
ui/src/components/Ellipsis/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Ellipsis from './Ellipsis'
|
||||||
|
|
||||||
|
export default Ellipsis
|
||||||
30
ui/src/components/FooterToolbar/FooterToolBar.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="prefixCls">
|
||||||
|
<div style="float: left">
|
||||||
|
<slot name="extra">{{ extra }}</slot>
|
||||||
|
</div>
|
||||||
|
<div style="float: right">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FooterToolBar',
|
||||||
|
props: {
|
||||||
|
prefixCls: {
|
||||||
|
type: String,
|
||||||
|
default: 'ant-pro-footer-toolbar'
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
4
ui/src/components/FooterToolbar/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import FooterToolBar from './FooterToolBar'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
export default FooterToolBar
|
||||||
23
ui/src/components/FooterToolbar/index.less
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@import "../index";
|
||||||
|
|
||||||
|
@footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar";
|
||||||
|
|
||||||
|
.@{footer-toolbar-prefix-cls} {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 56px;
|
||||||
|
line-height: 56px;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
padding: 0 24px;
|
||||||
|
z-index: 9;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
ui/src/components/IconSelector/IconSelector.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-tabs>
|
||||||
|
<a-tab-pane v-for="(v, i) in icons" :tab="v.title" :key="i">
|
||||||
|
<ul>
|
||||||
|
<li v-for="(icon, key) in v.icons" :key="`${v.title}-${key}`" :class="{ 'active': selectedIcon==icon }">
|
||||||
|
<a-icon :type="icon" :style="{ fontSize: '36px' }" @click="handleSelectedIcon(icon)" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import icons from './icons'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IconSelect',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
selectedIcon: '',
|
||||||
|
icons
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSelectedIcon (icon) {
|
||||||
|
this.selectedIcon = icon
|
||||||
|
this.$emit('change', icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
ul{
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: 250px;
|
||||||
|
li{
|
||||||
|
display: inline-block;
|
||||||
|
padding:5px;
|
||||||
|
margin:5px;
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&.active{
|
||||||
|
box-shadow: 0px 0px 5px 2px #888888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
26
ui/src/components/IconSelector/icons.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
title: '方向性图标',
|
||||||
|
icons: ['step-backward', 'step-forward', 'fast-backward', 'fast-forward', 'shrink', 'arrows-alt', 'down', 'up', 'left', 'right', 'caret-up', 'caret-down', 'caret-left', 'caret-right', 'up-circle', 'down-circle', 'left-circle', 'right-circle', 'double-right', 'double-left', 'vertical-left', 'vertical-right', 'forward', 'backward', 'rollback', 'enter', 'retweet', 'swap', 'swap-left', 'swap-right', 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'play-circle', 'up-square', 'down-square', 'left-square', 'right-square', 'login', 'logout', 'menu-fold', 'menu-unfold', 'border-bottom', 'border-horizontal', 'border-inner', 'border-left', 'border-right', 'border-top', 'border-verticle', 'pic-center', 'pic-left', 'pic-right', 'radius-bottomleft', 'radius-bottomright', 'radius-upleft', 'fullscreen', 'fullscreen-exit']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提示建议性图标',
|
||||||
|
icons: ['question', 'question-circle', 'plus', 'plus-circle', 'pause', 'pause-circle', 'minus', 'minus-circle', 'plus-square', 'minus-square', 'info', 'info-circle', 'exclamation', 'exclamation-circle', 'close', 'close-circle', 'close-square', 'check', 'check-circle', 'check-square', 'clock-circle', 'warning', 'issues-close', 'stop']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '编辑类图标',
|
||||||
|
icons: ['edit', 'form', 'copy', 'scissor', 'delete', 'snippets', 'diff', 'highlight', 'align-center', 'align-left', 'align-right', 'bg-colors', 'bold', 'italic', 'underline', 'strikethrough', 'redo', 'undo', 'zoom-in', 'zoom-out', 'font-colors', 'font-size', 'line-height', 'colum-height', 'dash', 'small-dash', 'sort-ascending', 'sort-descending', 'drag', 'ordered-list', 'radius-setting']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据类图标',
|
||||||
|
icons: ['area-chart', 'pie-chart', 'bar-chart', 'dot-chart', 'line-chart', 'radar-chart', 'heat-map', 'fall', 'rise', 'stock', 'box-plot', 'fund', 'sliders']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '网站通用图标',
|
||||||
|
icons: ['lock', 'unlock', 'bars', 'book', 'calendar', 'cloud', 'cloud-download', 'code', 'copy', 'credit-card', 'delete', 'desktop', 'download', 'ellipsis', 'file', 'file-text', 'file-unknown', 'file-pdf', 'file-word', 'file-excel', 'file-jpg', 'file-ppt', 'file-markdown', 'file-add', 'folder', 'folder-open', 'folder-add', 'hdd', 'frown', 'meh', 'smile', 'inbox', 'laptop', 'appstore', 'link', 'mail', 'mobile', 'notification', 'paper-clip', 'picture', 'poweroff', 'reload', 'search', 'setting', 'share-alt', 'shopping-cart', 'tablet', 'tag', 'tags', 'to-top', 'upload', 'user', 'video-camera', 'home', 'loading', 'loading-3-quarters', 'cloud-upload', 'star', 'heart', 'environment', 'eye', 'camera', 'save', 'team', 'solution', 'phone', 'filter', 'exception', 'export', 'customer-service', 'qrcode', 'scan', 'like', 'dislike', 'message', 'pay-circle', 'calculator', 'pushpin', 'bulb', 'select', 'switcher', 'rocket', 'bell', 'disconnect', 'database', 'compass', 'barcode', 'hourglass', 'key', 'flag', 'layout', 'printer', 'sound', 'usb', 'skin', 'tool', 'sync', 'wifi', 'car', 'schedule', 'user-add', 'user-delete', 'usergroup-add', 'usergroup-delete', 'man', 'woman', 'shop', 'gift', 'idcard', 'medicine-box', 'red-envelope', 'coffee', 'copyright', 'trademark', 'safety', 'wallet', 'bank', 'trophy', 'contacts', 'global', 'shake', 'api', 'fork', 'dashboard', 'table', 'profile', 'alert', 'audit', 'branches', 'build', 'border', 'crown', 'experiment', 'fire', 'money-collect', 'property-safety', 'read', 'reconciliation', 'rest', 'security-scan', 'insurance', 'interation', 'safety-certificate', 'project', 'thunderbolt', 'block', 'cluster', 'deployment-unit', 'dollar', 'euro', 'pound', 'file-done', 'file-exclamation', 'file-protect', 'file-search', 'file-sync', 'gateway', 'gold', 'robot', 'shopping']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '品牌和标识',
|
||||||
|
icons: ['android', 'apple', 'windows', 'ie', 'chrome', 'github', 'aliwangwang', 'dingding', 'weibo-square', 'weibo-circle', 'taobao-circle', 'html5', 'weibo', 'twitter', 'wechat', 'youtube', 'alipay-circle', 'taobao', 'skype', 'qq', 'medium-workmark', 'gitlab', 'medium', 'linkedin', 'google-plus', 'dropbox', 'facebook', 'codepen', 'code-sandbox', 'amazon', 'google', 'codepen-circle', 'alipay', 'ant-design', 'aliyun', 'zhihu', 'slack', 'slack-square', 'behance', 'behance-square', 'dribbble', 'dribbble-square', 'instagram', 'yuque', 'alibaba', 'yahoo']
|
||||||
|
}
|
||||||
|
]
|
||||||
2
ui/src/components/IconSelector/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import IconSelector from './IconSelector'
|
||||||
|
export default IconSelector
|
||||||
175
ui/src/components/MultiTab/MultiTab.vue
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
<!--
|
||||||
|
<template>
|
||||||
|
<div style="margin: -23px -24px 24px -24px">
|
||||||
|
<!–<a-dropdown :trigger="['contextmenu']" overlayClassName="multi-tab-menu-wrapper">
|
||||||
|
<a-menu slot="overlay">
|
||||||
|
<a-menu-item key="1">1st menu item</a-menu-item>
|
||||||
|
<a-menu-item key="2">2nd menu item</a-menu-item>
|
||||||
|
<a-menu-item key="3">3rd menu item</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-dropdown>–>
|
||||||
|
<a-tabs
|
||||||
|
hideAdd
|
||||||
|
v-model="activeKey"
|
||||||
|
type="editable-card"
|
||||||
|
:tabBarStyle="{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }"
|
||||||
|
@edit="onEdit"
|
||||||
|
>
|
||||||
|
<a-tab-pane v-for="page in pages" :style="{ height: 0 }" :tab="page.meta.title" :key="page.fullPath" :closable="pages.length > 1">
|
||||||
|
</a-tab-pane>
|
||||||
|
<template slot="renderTabBar" slot-scope="props, DefaultTabBar">
|
||||||
|
<component :is="DefaultTabBar" {...props} />
|
||||||
|
</template>
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'MultiTab',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
fullPathList: [],
|
||||||
|
pages: [],
|
||||||
|
activeKey: '',
|
||||||
|
newTabIndex: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.pages.push(this.$route)
|
||||||
|
this.fullPathList.push(this.$route.fullPath)
|
||||||
|
this.selectedLastPath()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onEdit (targetKey, action) {
|
||||||
|
this[action](targetKey)
|
||||||
|
},
|
||||||
|
remove (targetKey) {
|
||||||
|
this.pages = this.pages.filter(page => page.fullPath !== targetKey)
|
||||||
|
this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
|
||||||
|
// 判断当前标签是否关闭,若关闭则跳转到最后一个还存在的标签页
|
||||||
|
if (!this.fullPathList.includes(this.activeKey)) {
|
||||||
|
this.selectedLastPath()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedLastPath () {
|
||||||
|
this.activeKey = this.fullPathList[this.fullPathList.length - 1]
|
||||||
|
},
|
||||||
|
|
||||||
|
// content menu
|
||||||
|
closeThat (e) {
|
||||||
|
this.remove(e)
|
||||||
|
},
|
||||||
|
closeLeft (e) {
|
||||||
|
const currentIndex = this.fullPathList.indexOf(e)
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
this.fullPathList.forEach((item, index) => {
|
||||||
|
if (index < currentIndex) {
|
||||||
|
this.remove(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message.info('左侧没有标签')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeRight (e) {
|
||||||
|
const currentIndex = this.fullPathList.indexOf(e)
|
||||||
|
if (currentIndex < (this.fullPathList.length - 1)) {
|
||||||
|
this.fullPathList.forEach((item, index) => {
|
||||||
|
if (index > currentIndex) {
|
||||||
|
this.remove(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message.info('右侧没有标签')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeAll (e) {
|
||||||
|
const currentIndex = this.fullPathList.indexOf(e)
|
||||||
|
this.fullPathList.forEach((item, index) => {
|
||||||
|
if (index !== currentIndex) {
|
||||||
|
this.remove(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeMenuClick ({ key, item, domEvent }) {
|
||||||
|
const vkey = domEvent.target.getAttribute('data-vkey')
|
||||||
|
switch (key) {
|
||||||
|
case 'close-right':
|
||||||
|
this.closeRight(vkey)
|
||||||
|
break
|
||||||
|
case 'close-left':
|
||||||
|
this.closeLeft(vkey)
|
||||||
|
break
|
||||||
|
case 'close-all':
|
||||||
|
this.closeAll(vkey)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
case 'close-that':
|
||||||
|
this.closeThat(vkey)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderTabPaneMenu (e) {
|
||||||
|
return (
|
||||||
|
<a-menu {...{ on: { click: this.closeMenuClick } }}>
|
||||||
|
<a-menu-item key="close-that" data-vkey={e}>关闭当前标签</a-menu-item>
|
||||||
|
<a-menu-item key="close-right" data-vkey={e}>关闭右侧</a-menu-item>
|
||||||
|
<a-menu-item key="close-left" data-vkey={e}>关闭左侧</a-menu-item>
|
||||||
|
<a-menu-item key="close-all" data-vkey={e}>关闭全部</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// render
|
||||||
|
renderTabPane (title, keyPath) {
|
||||||
|
const menu = this.renderTabPaneMenu(keyPath)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a-dropdown overlay={menu} trigger={['contextmenu']}>
|
||||||
|
<span style={{ userSelect: 'none' }}>{ title }</span>
|
||||||
|
</a-dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route': function (newVal) {
|
||||||
|
this.activeKey = newVal.fullPath
|
||||||
|
if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
|
||||||
|
this.fullPathList.push(newVal.fullPath)
|
||||||
|
this.pages.push(newVal)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activeKey: function (newPathKey) {
|
||||||
|
this.$router.push({ path: newPathKey })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render () {
|
||||||
|
const { onEdit, $data: { pages } } = this
|
||||||
|
const panes = pages.map(page => {
|
||||||
|
return (
|
||||||
|
<a-tab-pane
|
||||||
|
style={{ height: 0 }}
|
||||||
|
tab={this.renderTabPane(page.meta.title, page.fullPath)}
|
||||||
|
key={page.fullPath} closable={pages.length > 1}
|
||||||
|
>
|
||||||
|
</a-tab-pane>)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="ant-pro-multi-tab">
|
||||||
|
<div class="ant-pro-multi-tab-wrapper">
|
||||||
|
<a-tabs
|
||||||
|
hideAdd
|
||||||
|
type={'editable-card'}
|
||||||
|
v-model={this.activeKey}
|
||||||
|
tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }}
|
||||||
|
{...{ on: { edit: onEdit } }}>
|
||||||
|
{panes}
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
4
ui/src/components/MultiTab/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import MultiTab from './MultiTab'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
export default MultiTab
|
||||||
20
ui/src/components/MultiTab/index.less
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
@import '../index';
|
||||||
|
|
||||||
|
@multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab";
|
||||||
|
@multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper";
|
||||||
|
|
||||||
|
/*
|
||||||
|
.topmenu .@{multi-tab-prefix-cls} {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: -23px auto 24px auto;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
.@{multi-tab-prefix-cls} {
|
||||||
|
margin: -23px -24px 24px -24px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topmenu .@{multi-tab-wrapper-prefix-cls} {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
54
ui/src/components/NumberInfo/NumberInfo.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="[prefixCls]">
|
||||||
|
<slot name="subtitle">
|
||||||
|
<div :class="[`${prefixCls}-subtitle`]">{{ typeof subTitle === 'string' ? subTitle : subTitle() }}</div>
|
||||||
|
</slot>
|
||||||
|
<div class="number-info-value">
|
||||||
|
<span>{{ total }}</span>
|
||||||
|
<span class="sub-total">
|
||||||
|
{{ subTotal }}
|
||||||
|
<icon :type="`caret-${status}`" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Icon from 'ant-design-vue/es/icon'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NumberInfo',
|
||||||
|
props: {
|
||||||
|
prefixCls: {
|
||||||
|
type: String,
|
||||||
|
default: 'ant-pro-number-info'
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
subTotal: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
subTitle: {
|
||||||
|
type: [String, Function],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
default: 'up'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Icon
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import "index";
|
||||||
|
</style>
|
||||||
3
ui/src/components/NumberInfo/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import NumberInfo from './NumberInfo'
|
||||||
|
|
||||||
|
export default NumberInfo
|
||||||
55
ui/src/components/NumberInfo/index.less
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
@import "../index";
|
||||||
|
|
||||||
|
@numberInfo-prefix-cls: ~"@{ant-pro-prefix}-number-info";
|
||||||
|
|
||||||
|
.@{numberInfo-prefix-cls} {
|
||||||
|
|
||||||
|
.ant-pro-number-info-subtitle {
|
||||||
|
color: @text-color-secondary;
|
||||||
|
font-size: @font-size-base;
|
||||||
|
height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-info-value {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
color: @heading-color;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-total {
|
||||||
|
color: @text-color-secondary;
|
||||||
|
font-size: @font-size-lg;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-right: 0;
|
||||||
|
i {
|
||||||
|
font-size: 12px;
|
||||||
|
transform: scale(0.82);
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
:global {
|
||||||
|
.anticon-caret-up {
|
||||||
|
color: @red-6;
|
||||||
|
}
|
||||||
|
.anticon-caret-down {
|
||||||
|
color: @green-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
ui/src/components/Result/Result.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="result">
|
||||||
|
<div>
|
||||||
|
<a-icon :class="{ 'icon': true, 'success': isSuccess, 'error': !isSuccess }" :type="isSuccess ? 'check-circle' : 'close-circle'"/>
|
||||||
|
</div>
|
||||||
|
<div class="title" v-if="title">{{ title }}</div>
|
||||||
|
<div class="description" v-if="description">{{ description }}</div>
|
||||||
|
<div class="content" v-if="content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<slot name="action"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Result',
|
||||||
|
props: {
|
||||||
|
isSuccess: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.result {
|
||||||
|
text-align: center;
|
||||||
|
width: 72%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px 0 8px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 72px;
|
||||||
|
line-height: 72px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
color: rgba(0, 0, 0, .85);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 32px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background: #fafafa;
|
||||||
|
padding: 24px 40px;
|
||||||
|
border-radius: 2px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.action {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.result {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
2
ui/src/components/Result/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import Result from './Result.vue'
|
||||||
|
export default Result
|
||||||
124
ui/src/components/Tree/Tree.jsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { Menu, Icon, Input } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const { Item, ItemGroup, SubMenu } = Menu
|
||||||
|
const { Search } = Input
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Tree',
|
||||||
|
props: {
|
||||||
|
dataSource: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
openKeys: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.localOpenKeys = this.openKeys.slice(0)
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
localOpenKeys: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handlePlus (item) {
|
||||||
|
this.$emit('add', item)
|
||||||
|
},
|
||||||
|
handleTitleClick (...args) {
|
||||||
|
this.$emit('titleClick', { args })
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSearch () {
|
||||||
|
return (
|
||||||
|
<Search
|
||||||
|
placeholder="input search text"
|
||||||
|
style="width: 100%; margin-bottom: 1rem"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
renderIcon (icon) {
|
||||||
|
return icon && (<Icon type={icon} />) || null
|
||||||
|
},
|
||||||
|
renderMenuItem (item) {
|
||||||
|
return (
|
||||||
|
<Item key={item.key}>
|
||||||
|
{ this.renderIcon(item.icon) }
|
||||||
|
{ item.title }
|
||||||
|
<a class="btn" style="width: 20px;z-index:1300" {...{ on: { click: () => this.handlePlus(item) } }}><a-icon type="plus"/></a>
|
||||||
|
</Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
renderItem (item) {
|
||||||
|
return item.children ? this.renderSubItem(item, item.key) : this.renderMenuItem(item, item.key)
|
||||||
|
},
|
||||||
|
renderItemGroup (item) {
|
||||||
|
const childrenItems = item.children.map(o => {
|
||||||
|
return this.renderItem(o, o.key)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemGroup key={item.key}>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{ item.title }</span>
|
||||||
|
<a-dropdown>
|
||||||
|
<a class="btn"><a-icon type="ellipsis" /></a>
|
||||||
|
<a-menu slot="overlay">
|
||||||
|
<a-menu-item key="1">新增</a-menu-item>
|
||||||
|
<a-menu-item key="2">合并</a-menu-item>
|
||||||
|
<a-menu-item key="3">移除</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-dropdown>
|
||||||
|
</template>
|
||||||
|
{ childrenItems }
|
||||||
|
</ItemGroup>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
renderSubItem (item, key) {
|
||||||
|
const childrenItems = item.children && item.children.map(o => {
|
||||||
|
return this.renderItem(o, o.key)
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = (
|
||||||
|
<span slot="title">
|
||||||
|
{ this.renderIcon(item.icon) }
|
||||||
|
<span>{ item.title }</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (item.group) {
|
||||||
|
return this.renderItemGroup(item)
|
||||||
|
}
|
||||||
|
// titleClick={this.handleTitleClick(item)}
|
||||||
|
return (
|
||||||
|
<SubMenu key={key}>
|
||||||
|
{ title }
|
||||||
|
{ childrenItems }
|
||||||
|
</SubMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render () {
|
||||||
|
const { dataSource, search } = this.$props
|
||||||
|
|
||||||
|
// this.localOpenKeys = openKeys.slice(0)
|
||||||
|
const list = dataSource.map(item => {
|
||||||
|
return this.renderItem(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="tree-wrapper">
|
||||||
|
{ search ? this.renderSearch() : null }
|
||||||
|
<Menu mode="inline" class="custom-tree" {...{ on: { click: item => this.$emit('click', item), 'update:openKeys': val => { this.localOpenKeys = val } } }} openKeys={this.localOpenKeys}>
|
||||||
|
{ list }
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
ui/src/components/Trend/Trend.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="[prefixCls, reverseColor && 'reverse-color' ]">
|
||||||
|
<span>
|
||||||
|
<slot name="term"></slot>
|
||||||
|
<span class="item-text">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span :class="[flag]"><a-icon :type="`caret-${flag}`"/></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Trend',
|
||||||
|
props: {
|
||||||
|
prefixCls: {
|
||||||
|
type: String,
|
||||||
|
default: 'ant-pro-trend'
|
||||||
|
},
|
||||||
|
flag: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
reverseColor: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import "index";
|
||||||
|
</style>
|
||||||
3
ui/src/components/Trend/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Trend from './Trend.vue'
|
||||||
|
|
||||||
|
export default Trend
|
||||||
42
ui/src/components/Trend/index.less
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@import "../index";
|
||||||
|
|
||||||
|
@trend-prefix-cls: ~"@{ant-pro-prefix}-trend";
|
||||||
|
|
||||||
|
.@{trend-prefix-cls} {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: @font-size-base;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
.up,
|
||||||
|
.down {
|
||||||
|
margin-left: 4px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 12px;
|
||||||
|
transform: scale(0.83);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-text {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: rgba(0,0,0,.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.up {
|
||||||
|
color: @red-6;
|
||||||
|
}
|
||||||
|
.down {
|
||||||
|
color: @green-6;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reverse-color .up {
|
||||||
|
color: @green-6;
|
||||||
|
}
|
||||||
|
&.reverse-color .down {
|
||||||
|
color: @red-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
ui/src/components/_util/util.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* components util
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function filterEmpty (children = []) {
|
||||||
|
return children.filter(c => c.tag || (c.text && c.text.trim() !== ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStrFullLength = (str = '') =>
|
||||||
|
str.split('').reduce((pre, cur) => {
|
||||||
|
const charCode = cur.charCodeAt(0)
|
||||||
|
if (charCode >= 0 && charCode <= 128) {
|
||||||
|
return pre + 1
|
||||||
|
}
|
||||||
|
return pre + 2
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
export const cutStrByFullLength = (str = '', maxLength) => {
|
||||||
|
let showLength = 0
|
||||||
|
return str.split('').reduce((pre, cur) => {
|
||||||
|
const charCode = cur.charCodeAt(0)
|
||||||
|
if (charCode >= 0 && charCode <= 128) {
|
||||||
|
showLength += 1
|
||||||
|
} else {
|
||||||
|
showLength += 2
|
||||||
|
}
|
||||||
|
if (showLength <= maxLength) {
|
||||||
|
return pre + cur
|
||||||
|
}
|
||||||
|
return pre
|
||||||
|
}, '')
|
||||||
|
}
|
||||||
57
ui/src/components/chart/Bar.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div :style="{ padding: '0 0 32px 32px' }">
|
||||||
|
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
|
||||||
|
<v-chart
|
||||||
|
height="254"
|
||||||
|
:data="data"
|
||||||
|
:forceFit="true"
|
||||||
|
:padding="['auto', 'auto', '40', '50']">
|
||||||
|
<v-tooltip />
|
||||||
|
<v-axis />
|
||||||
|
<v-bar position="x*y"/>
|
||||||
|
</v-chart>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const data = []
|
||||||
|
for (let i = 0; i < 12; i += 1) {
|
||||||
|
data.push({
|
||||||
|
x: `${i + 1}X`,
|
||||||
|
y: Math.floor(Math.random() * 1000) + 200
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const tooltip = [
|
||||||
|
'x*y',
|
||||||
|
(x, y) => ({
|
||||||
|
name: x,
|
||||||
|
value: y
|
||||||
|
})
|
||||||
|
]
|
||||||
|
const scale = [{
|
||||||
|
dataKey: 'x',
|
||||||
|
min: 2
|
||||||
|
}, {
|
||||||
|
dataKey: 'y',
|
||||||
|
title: 'Data',
|
||||||
|
min: 1,
|
||||||
|
max: 22
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Bar',
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
scale,
|
||||||
|
tooltip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
120
ui/src/components/chart/ChartCard.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<a-card :loading="loading" :body-style="{ padding: '20px 24px 8px' }" :bordered="false">
|
||||||
|
<div class="chart-card-header">
|
||||||
|
<div class="meta">
|
||||||
|
<span class="chart-card-title">
|
||||||
|
<slot name="title">
|
||||||
|
{{ title }}
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
<span class="chart-card-action">
|
||||||
|
<slot name="action"></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="total">
|
||||||
|
<slot name="total">
|
||||||
|
<span>{{ typeof total === 'function' && total() || total }}</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card-content">
|
||||||
|
<div class="content-fix">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card-footer">
|
||||||
|
<div class="field">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ChartCard',
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: [Function, Number, String],
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.chart-card-header {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
color: rgba(0, 0, 0, .45);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card-action {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card-footer {
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
padding-top: 9px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card-content {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
|
height: 46px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.content-fix {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #000;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 38px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
67
ui/src/components/chart/Liquid.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-chart
|
||||||
|
:forceFit="true"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
:data="data"
|
||||||
|
:scale="scale"
|
||||||
|
:padding="0">
|
||||||
|
<v-tooltip />
|
||||||
|
<v-interval
|
||||||
|
:shape="['liquid-fill-gauge']"
|
||||||
|
position="transfer*value"
|
||||||
|
color=""
|
||||||
|
:v-style="{
|
||||||
|
lineWidth: 10,
|
||||||
|
opacity: 0.75
|
||||||
|
}"
|
||||||
|
:tooltip="[
|
||||||
|
'transfer*value',
|
||||||
|
(transfer, value) => {
|
||||||
|
return {
|
||||||
|
name: transfer,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
></v-interval>
|
||||||
|
<v-guide
|
||||||
|
v-for="(row, index) in data"
|
||||||
|
:key="index"
|
||||||
|
type="text"
|
||||||
|
:top="true"
|
||||||
|
:position="{
|
||||||
|
gender: row.transfer,
|
||||||
|
value: 45
|
||||||
|
}"
|
||||||
|
:content="row.value + '%'"
|
||||||
|
:v-style="{
|
||||||
|
fontSize: 100,
|
||||||
|
textAlign: 'center',
|
||||||
|
opacity: 0.75,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</v-chart>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Liquid',
|
||||||
|
props: {
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
56
ui/src/components/chart/MiniArea.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div class="antv-chart-mini">
|
||||||
|
<div class="chart-wrapper" :style="{ height: 46 }">
|
||||||
|
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 0, 18, 0]">
|
||||||
|
<v-tooltip />
|
||||||
|
<v-smooth-area position="x*y" />
|
||||||
|
</v-chart>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment'
|
||||||
|
const data = []
|
||||||
|
const beginDay = new Date().getTime()
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
data.push({
|
||||||
|
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
|
||||||
|
y: Math.round(Math.random() * 10)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltip = [
|
||||||
|
'x*y',
|
||||||
|
(x, y) => ({
|
||||||
|
name: x,
|
||||||
|
value: y
|
||||||
|
})
|
||||||
|
]
|
||||||
|
const scale = [{
|
||||||
|
dataKey: 'x',
|
||||||
|
min: 2
|
||||||
|
}, {
|
||||||
|
dataKey: 'y',
|
||||||
|
title: '时间',
|
||||||
|
min: 1,
|
||||||
|
max: 22
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MiniArea',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
tooltip,
|
||||||
|
scale,
|
||||||
|
height: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import "chart";
|
||||||
|
</style>
|
||||||
57
ui/src/components/chart/MiniBar.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="antv-chart-mini">
|
||||||
|
<div class="chart-wrapper" :style="{ height: 46 }">
|
||||||
|
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
|
||||||
|
<v-tooltip />
|
||||||
|
<v-bar position="x*y" />
|
||||||
|
</v-chart>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment'
|
||||||
|
const data = []
|
||||||
|
const beginDay = new Date().getTime()
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
data.push({
|
||||||
|
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
|
||||||
|
y: Math.round(Math.random() * 10)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltip = [
|
||||||
|
'x*y',
|
||||||
|
(x, y) => ({
|
||||||
|
name: x,
|
||||||
|
value: y
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
const scale = [{
|
||||||
|
dataKey: 'x',
|
||||||
|
min: 2
|
||||||
|
}, {
|
||||||
|
dataKey: 'y',
|
||||||
|
title: '时间',
|
||||||
|
min: 1,
|
||||||
|
max: 30
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MiniBar',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
tooltip,
|
||||||
|
scale,
|
||||||
|
height: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import "chart";
|
||||||
|
</style>
|
||||||
75
ui/src/components/chart/MiniProgress.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart-mini-progress">
|
||||||
|
<div class="target" :style="{ left: target + '%'}">
|
||||||
|
<span :style="{ backgroundColor: color }" />
|
||||||
|
<span :style="{ backgroundColor: color }"/>
|
||||||
|
</div>
|
||||||
|
<div class="progress-wrapper">
|
||||||
|
<div class="progress" :style="{ backgroundColor: color, width: percentage + '%', height: height }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'MiniProgress',
|
||||||
|
props: {
|
||||||
|
target: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '10px'
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#13C2C2'
|
||||||
|
},
|
||||||
|
percentage: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.chart-mini-progress {
|
||||||
|
padding: 5px 0;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.target {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
border-radius: 100px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 4px;
|
||||||
|
width: 2px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.progress-wrapper {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
transition: all .4s cubic-bezier(.08,.82,.17,1) 0s;
|
||||||
|
border-radius: 1px 0 0 1px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
68
ui/src/components/chart/Radar.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<v-chart :forceFit="true" height="400" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
|
||||||
|
<v-tooltip></v-tooltip>
|
||||||
|
<v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid" />
|
||||||
|
<v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid" />
|
||||||
|
<v-legend dataKey="user" marker="circle" :offset="30" />
|
||||||
|
<v-coord type="polar" radius="0.8" />
|
||||||
|
<v-line position="item*score" color="user" :size="2" />
|
||||||
|
<v-point position="item*score" color="user" :size="4" shape="circle" />
|
||||||
|
</v-chart>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const axis1Opts = {
|
||||||
|
dataKey: 'item',
|
||||||
|
line: null,
|
||||||
|
tickLine: null,
|
||||||
|
grid: {
|
||||||
|
lineStyle: {
|
||||||
|
lineDash: null
|
||||||
|
},
|
||||||
|
hideFirstLine: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const axis2Opts = {
|
||||||
|
dataKey: 'score',
|
||||||
|
line: null,
|
||||||
|
tickLine: null,
|
||||||
|
grid: {
|
||||||
|
type: 'polygon',
|
||||||
|
lineStyle: {
|
||||||
|
lineDash: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scale = [
|
||||||
|
{
|
||||||
|
dataKey: 'score',
|
||||||
|
min: 0,
|
||||||
|
max: 80
|
||||||
|
}, {
|
||||||
|
dataKey: 'user',
|
||||||
|
alias: 'Alias'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Radar',
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
axis1Opts,
|
||||||
|
axis2Opts,
|
||||||
|
scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
77
ui/src/components/chart/RankList.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rank">
|
||||||
|
<h4 class="title">{{ title }}</h4>
|
||||||
|
<ul class="list">
|
||||||
|
<li :key="index" v-for="(item, index) in list">
|
||||||
|
<span :class="index < 3 ? 'active' : null">{{ index + 1 }}</span>
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
<span>{{ item.total }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'RankList',
|
||||||
|
// ['title', 'list']
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
.rank {
|
||||||
|
padding: 0 32px 32px 72px;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin: 25px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: rgba(0, 0, 0, .65);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 24px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: #314659;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .rank {
|
||||||
|
padding: 0 32px 32px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
64
ui/src/components/chart/TransferBar.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div :style="{ padding: '0 0 32px 32px' }">
|
||||||
|
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
|
||||||
|
<v-chart
|
||||||
|
height="254"
|
||||||
|
:data="data"
|
||||||
|
:scale="scale"
|
||||||
|
:forceFit="true"
|
||||||
|
:padding="['auto', 'auto', '40', '50']">
|
||||||
|
<v-tooltip />
|
||||||
|
<v-axis />
|
||||||
|
<v-bar position="x*y"/>
|
||||||
|
</v-chart>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const tooltip = [
|
||||||
|
'x*y',
|
||||||
|
(x, y) => ({
|
||||||
|
name: x,
|
||||||
|
value: y
|
||||||
|
})
|
||||||
|
]
|
||||||
|
const scale = [{
|
||||||
|
dataKey: 'x',
|
||||||
|
title: '日期(天)',
|
||||||
|
alias: '日期(天)',
|
||||||
|
min: 2
|
||||||
|
}, {
|
||||||
|
dataKey: 'y',
|
||||||
|
title: 'Title',
|
||||||
|
alias: 'Alias',
|
||||||
|
min: 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Bar',
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
scale,
|
||||||
|
tooltip
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getMonthBar()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getMonthBar () {
|
||||||
|
this.$http.get('/analysis/month-bar')
|
||||||
|
.then(res => {
|
||||||
|
this.data = res.result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
82
ui/src/components/chart/Trend.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart-trend">
|
||||||
|
{{ term }}
|
||||||
|
<span>{{ rate }}%</span>
|
||||||
|
<span :class="['trend-icon', trend]"><a-icon :type="'caret-' + trend"/></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Trend',
|
||||||
|
props: {
|
||||||
|
term: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
percentage: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: Boolean,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
fixed: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
trend: this.type && 'up' || 'down',
|
||||||
|
rate: this.percentage
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
const type = this.type === null ? this.value >= this.target : this.type
|
||||||
|
this.trend = type ? 'up' : 'down'
|
||||||
|
this.rate = (this.percentage === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percentage).toFixed(this.fixed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.chart-trend {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
.trend-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.up, &.down {
|
||||||
|
margin-left: 4px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 12px;
|
||||||
|
transform: scale(.83);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.up {
|
||||||
|
color: #f5222d;
|
||||||
|
}
|
||||||
|
&.down {
|
||||||
|
color: #52c41a;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
ui/src/components/chart/chart.less
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.antv-chart-mini {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -28px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
/* margin: 0 -5px;
|
||||||
|
overflow: hidden;*/
|
||||||
|
}
|
||||||
|
}
|
||||||
479
ui/src/components/global.less
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
@import './index.less';
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
&.colorWeak {
|
||||||
|
filter: invert(80%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.ant-layout {
|
||||||
|
height: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
&.mobile,&.tablet {
|
||||||
|
|
||||||
|
.ant-layout-content {
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 24px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-wrapper {
|
||||||
|
.ant-table-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.ant-table-body {
|
||||||
|
min-width: 800px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.topmenu {
|
||||||
|
&.content-width-Fluid {
|
||||||
|
.header-index-wide {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile {
|
||||||
|
.sidemenu {
|
||||||
|
.ant-header-fixedHeader {
|
||||||
|
|
||||||
|
&.ant-header-side-opened, &.ant-header-side-closed {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-layout-has-sider {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 64px;
|
||||||
|
padding: 0 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color .3s;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.025);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topmenu {
|
||||||
|
.ant-header-fixedHeader {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9;
|
||||||
|
width: 100%;
|
||||||
|
transition: width .2s;
|
||||||
|
|
||||||
|
&.ant-header-side-opened {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-header-side-closed {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.content-width-Fluid {
|
||||||
|
.header-index-wide {
|
||||||
|
max-width: unset;
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header-index-wide {
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidemenu {
|
||||||
|
.ant-header-fixedHeader {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9;
|
||||||
|
width: 100%;
|
||||||
|
transition: width .2s;
|
||||||
|
|
||||||
|
&.ant-header-side-opened {
|
||||||
|
width: calc(100% - 256px)
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-header-side-closed {
|
||||||
|
width: calc(100% - 80px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 12px 0 0;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header, .top-nav-header-index {
|
||||||
|
|
||||||
|
.user-wrapper {
|
||||||
|
float: right;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.action {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 12px;
|
||||||
|
display: inline-block;
|
||||||
|
transition: all .3s;
|
||||||
|
height: 100%;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin: 20px 8px 20px 0;
|
||||||
|
color: #1890ff;
|
||||||
|
background: hsla(0, 0%, 100%, .85);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
.user-wrapper {
|
||||||
|
|
||||||
|
.action {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
a {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile,&.tablet {
|
||||||
|
.top-nav-header-index {
|
||||||
|
|
||||||
|
.header-index-wide {
|
||||||
|
|
||||||
|
.header-index-left {
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo.top-nav-header {
|
||||||
|
flex: 0 0 56px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 58px;
|
||||||
|
h1 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
|
||||||
|
.header-index-wide {
|
||||||
|
|
||||||
|
.header-index-left {
|
||||||
|
.trigger {
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tablet {
|
||||||
|
// overflow: hidden; text-overflow:ellipsis; white-space: nowrap;
|
||||||
|
.top-nav-header-index {
|
||||||
|
|
||||||
|
.header-index-wide {
|
||||||
|
|
||||||
|
.header-index-left {
|
||||||
|
.logo > a {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-menu.ant-menu-horizontal {
|
||||||
|
flex: 1 1;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-nav-header-index {
|
||||||
|
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||||
|
position: relative;
|
||||||
|
transition: background .3s,width .2s;
|
||||||
|
|
||||||
|
.header-index-wide {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: auto;
|
||||||
|
padding-left: 0;
|
||||||
|
display: flex;
|
||||||
|
height: 64px;
|
||||||
|
|
||||||
|
.ant-menu.ant-menu-horizontal {
|
||||||
|
border: none;
|
||||||
|
height: 64px;
|
||||||
|
line-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-index-left {
|
||||||
|
flex: 1 1;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.logo.top-nav-header {
|
||||||
|
width: 165px;
|
||||||
|
height: 64px;
|
||||||
|
position: relative;
|
||||||
|
line-height: 64px;
|
||||||
|
transition: all .3s;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img, svg {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #fff;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 0 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-index-right {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 64px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.header-index-wide {
|
||||||
|
.header-index-left {
|
||||||
|
.logo {
|
||||||
|
h1 {
|
||||||
|
color: #002140;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-content {
|
||||||
|
margin: 24px 24px 0px;
|
||||||
|
height: 100%;
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.topmenu {
|
||||||
|
.page-header-index-wide {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer.drawer-sider {
|
||||||
|
.sider {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
.ant-drawer-content {
|
||||||
|
background-color: rgb(0, 21, 41);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.light {
|
||||||
|
box-shadow: none;
|
||||||
|
.ant-drawer-content {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-body {
|
||||||
|
padding: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sider {
|
||||||
|
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
.ant-layout-sider-children {
|
||||||
|
overflow-y: hidden;
|
||||||
|
&:hover{
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-fixed-sidemenu {
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 64px;
|
||||||
|
position: relative;
|
||||||
|
line-height: 64px;
|
||||||
|
padding-left: 24px;
|
||||||
|
-webkit-transition: all .3s;
|
||||||
|
transition: all .3s;
|
||||||
|
background: #002140;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img, svg, h1 {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 0 12px;
|
||||||
|
font-family: Avenir,Helvetica Neue,Arial,Helvetica,sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 2px 0px 8px 0px rgba(29, 35, 41, 0.05);
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 1px 1px 0px 0px #e8e8e8;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-light {
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-menu {
|
||||||
|
span {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.user-dropdown-menu-wrapper.ant-dropdown-menu {
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.ant-dropdown-menu-item {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-menu-item > .anticon:first-child,
|
||||||
|
.ant-dropdown-menu-item > a > .anticon:first-child,
|
||||||
|
.ant-dropdown-menu-submenu-title > .anticon:first-child
|
||||||
|
.ant-dropdown-menu-submenu-title > a > .anticon:first-child {
|
||||||
|
min-width: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-page-search-wrapper {
|
||||||
|
|
||||||
|
.ant-form-inline {
|
||||||
|
|
||||||
|
.ant-form-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
margin-right: 0;
|
||||||
|
|
||||||
|
.ant-form-item-control-wrapper {
|
||||||
|
flex: 1 1;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
>.ant-form-item-label {
|
||||||
|
line-height: 32px;
|
||||||
|
padding-right: 8px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.ant-form-item-control {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-page-search-submitButtons {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
|
||||||
|
.table-operator {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
ui/src/components/index.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// chart
|
||||||
|
import Bar from '@/components/chart/Bar'
|
||||||
|
import ChartCard from '@/components/chart/ChartCard'
|
||||||
|
import Liquid from '@/components/chart/Liquid'
|
||||||
|
import MiniArea from '@/components/chart/MiniArea'
|
||||||
|
import MiniBar from '@/components/chart/MiniBar'
|
||||||
|
import MiniProgress from '@/components/chart/MiniProgress'
|
||||||
|
import Radar from '@/components/chart/Radar'
|
||||||
|
import RankList from '@/components/chart/RankList'
|
||||||
|
import TransferBar from '@/components/chart/TransferBar'
|
||||||
|
|
||||||
|
// pro components
|
||||||
|
import AvatarList from '@/components/AvatarList'
|
||||||
|
import CountDown from '@/components/CountDown'
|
||||||
|
import Ellipsis from '@/components/Ellipsis'
|
||||||
|
import FooterToolbar from '@/components/FooterToolbar'
|
||||||
|
import NumberInfo from '@/components/NumberInfo'
|
||||||
|
import DetailList from '@/components/tools/DetailList'
|
||||||
|
import Tree from '@/components/Tree/Tree'
|
||||||
|
import Trend from '@/components/Trend'
|
||||||
|
import STable from '@/components/table'
|
||||||
|
import MultiTab from '@/components/MultiTab'
|
||||||
|
import Result from '@/components/Result'
|
||||||
|
import IconSelector from '@/components/IconSelector'
|
||||||
|
|
||||||
|
export {
|
||||||
|
AvatarList,
|
||||||
|
Bar,
|
||||||
|
ChartCard,
|
||||||
|
Liquid,
|
||||||
|
MiniArea,
|
||||||
|
MiniBar,
|
||||||
|
MiniProgress,
|
||||||
|
Radar,
|
||||||
|
RankList,
|
||||||
|
TransferBar,
|
||||||
|
Trend,
|
||||||
|
CountDown,
|
||||||
|
Ellipsis,
|
||||||
|
FooterToolbar,
|
||||||
|
NumberInfo,
|
||||||
|
DetailList,
|
||||||
|
Tree,
|
||||||
|
STable,
|
||||||
|
MultiTab,
|
||||||
|
Result,
|
||||||
|
IconSelector
|
||||||
|
}
|
||||||
4
ui/src/components/index.less
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@import "~ant-design-vue/lib/style/index";
|
||||||
|
|
||||||
|
// The prefix to use on all css classes from ant-pro.
|
||||||
|
@ant-pro-prefix : ant-pro;
|
||||||
53
ui/src/components/layouts/BasicLayout.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<global-layout>
|
||||||
|
<multi-tab v-if="$store.getters.multiTab"></multi-tab>
|
||||||
|
<transition name="page-transition">
|
||||||
|
<route-view />
|
||||||
|
</transition>
|
||||||
|
</global-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RouteView from '@/components/layouts/RouteView'
|
||||||
|
import MultiTab from '@/components/MultiTab'
|
||||||
|
import GlobalLayout from '@/components/page/GlobalLayout'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BasicLayout',
|
||||||
|
components: {
|
||||||
|
RouteView,
|
||||||
|
MultiTab,
|
||||||
|
GlobalLayout
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@import url('../global.less');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The following styles are auto-applied to elements with
|
||||||
|
* transition="page-transition" when their visibility is toggled
|
||||||
|
* by Vue.js.
|
||||||
|
*
|
||||||
|
* You can easily play with the page transition by editing
|
||||||
|
* these styles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.page-transition-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-transition-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-transition-enter .page-transition-container,
|
||||||
|
.page-transition-leave-active .page-transition-container {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
16
ui/src/components/layouts/BlankLayout.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BlankLayout'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
149
ui/src/components/layouts/PageView.vue
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div :style="!$route.meta.hiddenHeaderContent ? 'margin: -24px -24px 0px;' : null">
|
||||||
|
<div class="page-menu-tabs">
|
||||||
|
<a-tabs :tabBarStyle="{margin: 0}" @change="changeTab">
|
||||||
|
<a-tab-pane v-for="child in $route.permission" :tab="child.meta.title" :key="child.key"></a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="page-header-index-wide">
|
||||||
|
<slot>
|
||||||
|
<!-- keep-alive -->
|
||||||
|
<keep-alive v-if="multiTab">
|
||||||
|
<router-view ref="content" />
|
||||||
|
</keep-alive>
|
||||||
|
<router-view v-else ref="content" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageView',
|
||||||
|
created () {
|
||||||
|
console.log(this.$route)
|
||||||
|
console.log(this.$router)
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
avatar: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
pageTitle: null,
|
||||||
|
description: null,
|
||||||
|
linkList: [],
|
||||||
|
extraImage: '',
|
||||||
|
search: false,
|
||||||
|
tabs: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
multiTab: state => state.app.multiTab
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.getPageMeta()
|
||||||
|
},
|
||||||
|
updated () {
|
||||||
|
this.getPageMeta()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeTab () {
|
||||||
|
|
||||||
|
},
|
||||||
|
getPageMeta () {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.pageTitle = (typeof(this.title) === 'string' || !this.title) ? this.title : this.$route.meta.title
|
||||||
|
|
||||||
|
const content = this.$refs.content
|
||||||
|
if (content) {
|
||||||
|
if (content.pageMeta) {
|
||||||
|
Object.assign(this, content.pageMeta)
|
||||||
|
} else {
|
||||||
|
this.description = content.description
|
||||||
|
this.linkList = content.linkList
|
||||||
|
this.extraImage = content.extraImage
|
||||||
|
this.search = content.search === true
|
||||||
|
this.tabs = content.tabs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.content {
|
||||||
|
margin: 24px 24px 0;
|
||||||
|
.link {
|
||||||
|
margin-top: 16px;
|
||||||
|
&:not(:empty) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
margin-right: 32px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
display: inline-block;
|
||||||
|
i {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.page-menu-search {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.page-menu-tabs {
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra-img {
|
||||||
|
margin-top: -60px;
|
||||||
|
text-align: center;
|
||||||
|
width: 195px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.extra-img{
|
||||||
|
margin-top: 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 96px;
|
||||||
|
|
||||||
|
img{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
23
ui/src/components/layouts/RouteView.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'RouteView',
|
||||||
|
data () {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
render () {
|
||||||
|
const { $route: { meta }, $store: { getters } } = this
|
||||||
|
const inKeep = (
|
||||||
|
<keep-alive>
|
||||||
|
<router-view />
|
||||||
|
</keep-alive>
|
||||||
|
)
|
||||||
|
const notKeep = (
|
||||||
|
<router-view />
|
||||||
|
)
|
||||||
|
if (meta.keepAlive === false) {
|
||||||
|
return notKeep
|
||||||
|
}
|
||||||
|
return getters.multiTab || meta.keepAlive ? inKeep : notKeep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
130
ui/src/components/layouts/UserLayout.vue
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
<template>
|
||||||
|
<div id="userLayout" :class="['user-layout-wrapper', device]">
|
||||||
|
<div class="container">
|
||||||
|
<div class="top">
|
||||||
|
<div class="header">
|
||||||
|
<img src="~@/assets/banner.png" class="logo" alt="logo">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<route-view></route-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RouteView from '@/components/layouts/RouteView'
|
||||||
|
import { mixinDevice } from '@/utils/mixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UserLayout',
|
||||||
|
components: { RouteView },
|
||||||
|
mixins: [mixinDevice],
|
||||||
|
data () {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.body.classList.add('userLayout')
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
document.body.classList.remove('userLayout')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
#userLayout.user-layout-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.mobile {
|
||||||
|
.container {
|
||||||
|
padding-top: 50px;
|
||||||
|
|
||||||
|
.main {
|
||||||
|
max-width: 368px;
|
||||||
|
width: 98%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
background: #fff; // url(~@/assets/background.svg) no-repeat 50%;
|
||||||
|
background-size: 100%;
|
||||||
|
padding-top: 75px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: -12px;
|
||||||
|
margin-top: -10px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
vertical-align: top;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 33px;
|
||||||
|
color: rgba(0, 0, 0, .85);
|
||||||
|
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
min-width: 260px;
|
||||||
|
width: 368px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
margin: 48px 0 24px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.links {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
a {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
transition: all 0.3s;
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.copyright {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
ui/src/components/layouts/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import UserLayout from './UserLayout'
|
||||||
|
import BlankLayout from './BlankLayout'
|
||||||
|
import BasicLayout from './BasicLayout'
|
||||||
|
import RouteView from './RouteView'
|
||||||
|
import PageView from './PageView'
|
||||||
|
|
||||||
|
export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView }
|
||||||
62
ui/src/components/menu/SideMenu.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<a-layout-sider
|
||||||
|
:class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null ]"
|
||||||
|
width="256px"
|
||||||
|
:collapsible="collapsible"
|
||||||
|
v-model="collapsed"
|
||||||
|
:trigger="null">
|
||||||
|
<logo />
|
||||||
|
<s-menu
|
||||||
|
:collapsed="collapsed"
|
||||||
|
:menu="menus"
|
||||||
|
:theme="theme"
|
||||||
|
:mode="mode"
|
||||||
|
@select="onSelect"
|
||||||
|
style="padding: 16px 0px;"></s-menu>
|
||||||
|
</a-layout-sider>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ALayoutSider from 'ant-design-vue/es/layout/Sider'
|
||||||
|
import Logo from '../tools/Logo'
|
||||||
|
import SMenu from './index'
|
||||||
|
import { mixin, mixinDevice } from '@/utils/mixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SideMenu',
|
||||||
|
components: { ALayoutSider, Logo, SMenu },
|
||||||
|
mixins: [mixin, mixinDevice],
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'inline'
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'dark'
|
||||||
|
},
|
||||||
|
collapsible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
menus: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSelect (obj) {
|
||||||
|
this.$emit('menuSelect', obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
2
ui/src/components/menu/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import SMenu from './menu'
|
||||||
|
export default SMenu
|
||||||
169
ui/src/components/menu/menu.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import Menu from 'ant-design-vue/es/menu'
|
||||||
|
import Icon from 'ant-design-vue/es/icon'
|
||||||
|
|
||||||
|
const { Item, SubMenu } = Menu
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SMenu',
|
||||||
|
props: {
|
||||||
|
menu: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'dark'
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'inline'
|
||||||
|
},
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
openKeys: [],
|
||||||
|
selectedKeys: [],
|
||||||
|
cachedOpenKeys: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
rootSubmenuKeys: vm => {
|
||||||
|
const keys = []
|
||||||
|
vm.menu.forEach(item => keys.push(item.path))
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.updateMenu()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
collapsed (val) {
|
||||||
|
if (val) {
|
||||||
|
this.cachedOpenKeys = this.openKeys.concat()
|
||||||
|
this.openKeys = []
|
||||||
|
} else {
|
||||||
|
this.openKeys = this.cachedOpenKeys
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$route: function () {
|
||||||
|
this.updateMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// select menu item
|
||||||
|
onOpenChange (openKeys) {
|
||||||
|
if (this.mode === 'horizontal') {
|
||||||
|
this.openKeys = openKeys
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
|
||||||
|
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
|
||||||
|
this.openKeys = openKeys
|
||||||
|
} else {
|
||||||
|
this.openKeys = latestOpenKey ? [latestOpenKey] : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateMenu () {
|
||||||
|
const routes = this.$route.matched.concat()
|
||||||
|
|
||||||
|
if (routes.length >= 4 && this.$route.meta.hidden) {
|
||||||
|
routes.pop()
|
||||||
|
this.selectedKeys = [routes[2].path]
|
||||||
|
} else {
|
||||||
|
this.selectedKeys = [routes.pop().path]
|
||||||
|
}
|
||||||
|
|
||||||
|
const openKeys = []
|
||||||
|
if (this.mode === 'inline') {
|
||||||
|
routes.forEach(item => {
|
||||||
|
openKeys.push(item.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
|
||||||
|
},
|
||||||
|
|
||||||
|
// render
|
||||||
|
renderItem (menu) {
|
||||||
|
if (!menu.hidden) {
|
||||||
|
return menu.children && !menu.hideChildrenInMenu ? this.renderSubMenu(menu) : this.renderMenuItem(menu)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
renderMenuItem (menu) {
|
||||||
|
const target = menu.meta.target || null
|
||||||
|
const props = {
|
||||||
|
to: { name: menu.name },
|
||||||
|
target: target
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Item {...{ key: menu.path }}>
|
||||||
|
<router-link {...{ props }}>
|
||||||
|
{this.renderIcon(menu.meta.icon)}
|
||||||
|
<span>{menu.meta.title}</span>
|
||||||
|
</router-link>
|
||||||
|
</Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
renderSubMenu (menu) {
|
||||||
|
const itemArr = []
|
||||||
|
if (!menu.hideChildrenInMenu) {
|
||||||
|
menu.children.forEach(item => itemArr.push(this.renderItem(item)))
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SubMenu {...{ key: menu.path }}>
|
||||||
|
<span slot="title">
|
||||||
|
{this.renderIcon(menu.meta.icon)}
|
||||||
|
<span>{menu.meta.title}</span>
|
||||||
|
</span>
|
||||||
|
{itemArr}
|
||||||
|
</SubMenu>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
renderIcon (icon) {
|
||||||
|
if (icon === 'none' || icon === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const props = {}
|
||||||
|
typeof (icon) === 'object' ? props.component = icon : props.type = icon
|
||||||
|
return (
|
||||||
|
<Icon {... { props } }/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render () {
|
||||||
|
const { mode, theme, menu } = this
|
||||||
|
const props = {
|
||||||
|
mode: mode,
|
||||||
|
theme: theme,
|
||||||
|
openKeys: this.openKeys
|
||||||
|
}
|
||||||
|
const on = {
|
||||||
|
select: obj => {
|
||||||
|
this.selectedKeys = obj.selectedKeys
|
||||||
|
this.$emit('select', obj)
|
||||||
|
},
|
||||||
|
openChange: this.onOpenChange
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuTree = menu.map(item => {
|
||||||
|
if (item.hidden) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.renderItem(item)
|
||||||
|
})
|
||||||
|
// {...{ props, on: on }}
|
||||||
|
return (
|
||||||
|
<Menu vModel={this.selectedKeys} {...{ props, on: on }}>
|
||||||
|
{menuTree}
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
156
ui/src/components/menu/menu.render.js
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import Menu from 'ant-design-vue/es/menu'
|
||||||
|
import Icon from 'ant-design-vue/es/icon'
|
||||||
|
|
||||||
|
const { Item, SubMenu } = Menu
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SMenu',
|
||||||
|
props: {
|
||||||
|
menu: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'dark'
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'inline'
|
||||||
|
},
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
openKeys: [],
|
||||||
|
selectedKeys: [],
|
||||||
|
cachedOpenKeys: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
rootSubmenuKeys: vm => {
|
||||||
|
const keys = []
|
||||||
|
vm.menu.forEach(item => keys.push(item.path))
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.updateMenu()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
collapsed (val) {
|
||||||
|
if (val) {
|
||||||
|
this.cachedOpenKeys = this.openKeys.concat()
|
||||||
|
this.openKeys = []
|
||||||
|
} else {
|
||||||
|
this.openKeys = this.cachedOpenKeys
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$route: function () {
|
||||||
|
this.updateMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
renderIcon: function (h, icon) {
|
||||||
|
if (icon === 'none' || icon === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const props = {}
|
||||||
|
typeof (icon) === 'object' ? props.component = icon : props.type = icon
|
||||||
|
return h(Icon, { props: { ...props } })
|
||||||
|
},
|
||||||
|
renderMenuItem: function (h, menu, pIndex, index) {
|
||||||
|
const target = menu.meta.target || null
|
||||||
|
return h(Item, { key: menu.path ? menu.path : 'item_' + pIndex + '_' + index }, [
|
||||||
|
h('router-link', { attrs: { to: { name: menu.name }, target: target } }, [
|
||||||
|
this.renderIcon(h, menu.meta.icon),
|
||||||
|
h('span', [menu.meta.title])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
},
|
||||||
|
renderSubMenu: function (h, menu, pIndex, index) {
|
||||||
|
const this2_ = this
|
||||||
|
const subItem = [h('span', { slot: 'title' }, [this.renderIcon(h, menu.meta.icon), h('span', [menu.meta.title])])]
|
||||||
|
const itemArr = []
|
||||||
|
const pIndex_ = pIndex + '_' + index
|
||||||
|
console.log('menu', menu)
|
||||||
|
if (!menu.hideChildrenInMenu) {
|
||||||
|
menu.children.forEach(function (item, i) {
|
||||||
|
itemArr.push(this2_.renderItem(h, item, pIndex_, i))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return h(SubMenu, { key: menu.path ? menu.path : 'submenu_' + pIndex + '_' + index }, subItem.concat(itemArr))
|
||||||
|
},
|
||||||
|
renderItem: function (h, menu, pIndex, index) {
|
||||||
|
if (!menu.hidden) {
|
||||||
|
return menu.children && !menu.hideChildrenInMenu
|
||||||
|
? this.renderSubMenu(h, menu, pIndex, index)
|
||||||
|
: this.renderMenuItem(h, menu, pIndex, index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderMenu: function (h, menuTree) {
|
||||||
|
const this2_ = this
|
||||||
|
const menuArr = []
|
||||||
|
menuTree.forEach(function (menu, i) {
|
||||||
|
if (!menu.hidden) {
|
||||||
|
menuArr.push(this2_.renderItem(h, menu, '0', i))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return menuArr
|
||||||
|
},
|
||||||
|
onOpenChange (openKeys) {
|
||||||
|
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
|
||||||
|
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
|
||||||
|
this.openKeys = openKeys
|
||||||
|
} else {
|
||||||
|
this.openKeys = latestOpenKey ? [latestOpenKey] : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateMenu () {
|
||||||
|
const routes = this.$route.matched.concat()
|
||||||
|
|
||||||
|
if (routes.length >= 4 && this.$route.meta.hidden) {
|
||||||
|
routes.pop()
|
||||||
|
this.selectedKeys = [routes[2].path]
|
||||||
|
} else {
|
||||||
|
this.selectedKeys = [routes.pop().path]
|
||||||
|
}
|
||||||
|
|
||||||
|
const openKeys = []
|
||||||
|
if (this.mode === 'inline') {
|
||||||
|
routes.forEach(item => {
|
||||||
|
openKeys.push(item.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render (h) {
|
||||||
|
return h(
|
||||||
|
Menu,
|
||||||
|
{
|
||||||
|
props: {
|
||||||
|
theme: this.$props.theme,
|
||||||
|
mode: this.$props.mode,
|
||||||
|
openKeys: this.openKeys,
|
||||||
|
selectedKeys: this.selectedKeys
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
openChange: this.onOpenChange,
|
||||||
|
select: obj => {
|
||||||
|
this.selectedKeys = obj.selectedKeys
|
||||||
|
this.$emit('select', obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.renderMenu(h, this.menu)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
48
ui/src/components/page/GlobalFooter.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="footer">
|
||||||
|
<div class="links">
|
||||||
|
<a href="https://github.com/apache/cloudstack" target="_blank">
|
||||||
|
<a-icon type="github"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/rhtyd">rhtyd</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'LayoutFooter',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.footer {
|
||||||
|
padding: 0 16px;
|
||||||
|
margin: 48px 0 24px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.links {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(0, 0, 0, .45);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, .65);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.copyright {
|
||||||
|
color: rgba(0, 0, 0, .45);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
157
ui/src/components/page/GlobalHeader.vue
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<a-layout-header v-if="!headerBarFixed" :class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', theme ]" :style="{ padding: '0' }">
|
||||||
|
<div v-if="mode === 'sidemenu'" class="header">
|
||||||
|
<a-icon
|
||||||
|
v-if="device==='mobile'"
|
||||||
|
class="trigger"
|
||||||
|
:type="collapsed ? 'menu-fold' : 'menu-unfold'"
|
||||||
|
@click="toggle"></a-icon>
|
||||||
|
<a-icon
|
||||||
|
v-else
|
||||||
|
class="trigger"
|
||||||
|
:type="collapsed ? 'menu-unfold' : 'menu-fold'"
|
||||||
|
@click="toggle"/>
|
||||||
|
|
||||||
|
<a-breadcrumb class="breadcrumb" v-if="device !== 'mobile'">
|
||||||
|
<a-breadcrumb-item v-for="(item, index) in breadList" :key="index">
|
||||||
|
<router-link
|
||||||
|
v-if="item.name"
|
||||||
|
:to="{ path: item.path === '' ? '/' : item.path }"
|
||||||
|
>
|
||||||
|
<a-icon v-if="index == 0" :type="item.meta.icon" />
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</router-link>
|
||||||
|
<span v-else-if="$route.params.id">{{ $route.params.id }}</span>
|
||||||
|
<span v-else>{{ item.meta.title }}</span>
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
<user-menu></user-menu>
|
||||||
|
</div>
|
||||||
|
<div v-else :class="['top-nav-header-index', theme]">
|
||||||
|
<div class="header-index-wide">
|
||||||
|
<div class="header-index-left">
|
||||||
|
<logo class="top-nav-header" :show-title="device !== 'mobile'" />
|
||||||
|
<s-menu
|
||||||
|
v-if="device !== 'mobile'"
|
||||||
|
mode="horizontal"
|
||||||
|
:menu="menus"
|
||||||
|
:theme="theme"
|
||||||
|
></s-menu>
|
||||||
|
<a-icon
|
||||||
|
v-else
|
||||||
|
class="trigger"
|
||||||
|
:type="collapsed ? 'menu-fold' : 'menu-unfold'"
|
||||||
|
@click="toggle"></a-icon>
|
||||||
|
</div>
|
||||||
|
<a-breadcrumb class="breadcrumb" v-if="device !== 'mobile'">
|
||||||
|
<a-breadcrumb-item v-for="(item, index) in breadList" :key="index">
|
||||||
|
<router-link
|
||||||
|
v-if="item.name"
|
||||||
|
:to="{ path: item.path === '' ? '/' : item.path }"
|
||||||
|
>
|
||||||
|
<a-icon v-if="index == 0" :type="item.meta.icon" />
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</router-link>
|
||||||
|
<span v-else-if="$route.params.id">{{ $route.params.id }}</span>
|
||||||
|
<span v-else>{{ item.meta.title }}</span>
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
<user-menu class="header-index-right"></user-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</a-layout-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import UserMenu from '../tools/UserMenu'
|
||||||
|
import SMenu from '../menu/'
|
||||||
|
import Logo from '../tools/Logo'
|
||||||
|
import Breadcrumb from '@/components/tools/Breadcrumb'
|
||||||
|
|
||||||
|
import { mixin } from '@/utils/mixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GlobalHeader',
|
||||||
|
components: {
|
||||||
|
SMenu,
|
||||||
|
Logo,
|
||||||
|
UserMenu,
|
||||||
|
Breadcrumb
|
||||||
|
},
|
||||||
|
mixins: [mixin],
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
// sidemenu, topmenu
|
||||||
|
default: 'sidemenu'
|
||||||
|
},
|
||||||
|
menus: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'dark'
|
||||||
|
},
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
device: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'desktop'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
headerBarFixed: false,
|
||||||
|
breadList: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route () {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
window.addEventListener('scroll', this.handleScroll)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getBreadcrumb () {
|
||||||
|
this.breadList = []
|
||||||
|
this.name = this.$route.name
|
||||||
|
this.$route.matched.forEach((item) => {
|
||||||
|
this.breadList.push(item)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleScroll () {
|
||||||
|
if (this.autoHideHeader) {
|
||||||
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
|
||||||
|
if (scrollTop > 100) {
|
||||||
|
this.headerBarFixed = true
|
||||||
|
} else {
|
||||||
|
this.headerBarFixed = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.headerBarFixed = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggle () {
|
||||||
|
this.$emit('toggle')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.breadcrumb {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
154
ui/src/components/page/GlobalLayout.vue
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<a-layout class="layout" :class="[device]">
|
||||||
|
|
||||||
|
<template v-if="isSideMenu()">
|
||||||
|
<a-drawer
|
||||||
|
v-if="isMobile()"
|
||||||
|
:wrapClassName="'drawer-sider ' + navTheme"
|
||||||
|
:closable="false"
|
||||||
|
:visible="collapsed"
|
||||||
|
placement="left"
|
||||||
|
@close="() => this.collapsed = false"
|
||||||
|
>
|
||||||
|
<side-menu
|
||||||
|
:menus="menus"
|
||||||
|
:theme="navTheme"
|
||||||
|
:collapsed="false"
|
||||||
|
:collapsible="true"
|
||||||
|
mode="inline"
|
||||||
|
@menuSelect="menuSelect"></side-menu>
|
||||||
|
</a-drawer>
|
||||||
|
|
||||||
|
<side-menu
|
||||||
|
v-else
|
||||||
|
mode="inline"
|
||||||
|
:menus="menus"
|
||||||
|
:theme="navTheme"
|
||||||
|
:collapsed="collapsed"
|
||||||
|
:collapsible="true"></side-menu>
|
||||||
|
</template>
|
||||||
|
<!-- 下次优化这些代码 -->
|
||||||
|
<template v-else>
|
||||||
|
<a-drawer
|
||||||
|
v-if="isMobile()"
|
||||||
|
:wrapClassName="'drawer-sider ' + navTheme"
|
||||||
|
placement="left"
|
||||||
|
@close="() => this.collapsed = false"
|
||||||
|
:closable="false"
|
||||||
|
:visible="collapsed"
|
||||||
|
>
|
||||||
|
<side-menu
|
||||||
|
:menus="menus"
|
||||||
|
:theme="navTheme"
|
||||||
|
:collapsed="false"
|
||||||
|
:collapsible="true"
|
||||||
|
mode="inline"
|
||||||
|
@menuSelect="menuSelect"></side-menu>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-layout :class="[layoutMode, `content-width-${contentWidth}`]" :style="{ paddingLeft: contentPaddingLeft, minHeight: '100vh' }">
|
||||||
|
<!-- layout header -->
|
||||||
|
<global-header
|
||||||
|
:mode="layoutMode"
|
||||||
|
:menus="menus"
|
||||||
|
:theme="navTheme"
|
||||||
|
:collapsed="collapsed"
|
||||||
|
:device="device"
|
||||||
|
@toggle="toggle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- layout content -->
|
||||||
|
<a-layout-content :style="{ margin: $store.getters.multiTab ? '24px 24px 0' : '24px 24px 0', height: '100%', paddingTop: fixedHeader ? '40px' : '0' }">
|
||||||
|
<slot></slot>
|
||||||
|
</a-layout-content>
|
||||||
|
|
||||||
|
<!-- layout footer -->
|
||||||
|
<a-layout-footer style="padding: 0">
|
||||||
|
<global-footer />
|
||||||
|
</a-layout-footer>
|
||||||
|
</a-layout>
|
||||||
|
</a-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SideMenu from '@/components/menu/SideMenu'
|
||||||
|
import GlobalHeader from '@/components/page/GlobalHeader'
|
||||||
|
import GlobalFooter from '@/components/page/GlobalFooter'
|
||||||
|
import { triggerWindowResizeEvent } from '@/utils/util'
|
||||||
|
import { mapState, mapActions } from 'vuex'
|
||||||
|
import { mixin, mixinDevice } from '@/utils/mixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GlobalLayout',
|
||||||
|
components: {
|
||||||
|
SideMenu,
|
||||||
|
GlobalHeader,
|
||||||
|
GlobalFooter
|
||||||
|
},
|
||||||
|
mixins: [mixin, mixinDevice],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
collapsed: false,
|
||||||
|
menus: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
// 主路由
|
||||||
|
mainMenu: state => state.permission.addRouters
|
||||||
|
}),
|
||||||
|
contentPaddingLeft () {
|
||||||
|
if (!this.fixSidebar || this.isMobile()) {
|
||||||
|
return '0'
|
||||||
|
}
|
||||||
|
if (this.sidebarOpened) {
|
||||||
|
return '256px'
|
||||||
|
}
|
||||||
|
return '80px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
sidebarOpened (val) {
|
||||||
|
this.collapsed = !val
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.menus = this.mainMenu.find((item) => item.path === '/').children
|
||||||
|
this.collapsed = !this.sidebarOpened
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
const userAgent = navigator.userAgent
|
||||||
|
if (userAgent.indexOf('Edge') > -1) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.collapsed = !this.collapsed
|
||||||
|
setTimeout(() => {
|
||||||
|
this.collapsed = !this.collapsed
|
||||||
|
}, 16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['setSidebar']),
|
||||||
|
toggle () {
|
||||||
|
this.collapsed = !this.collapsed
|
||||||
|
this.setSidebar(!this.collapsed)
|
||||||
|
triggerWindowResizeEvent()
|
||||||
|
},
|
||||||
|
paddingCalc () {
|
||||||
|
let left = ''
|
||||||
|
if (this.sidebarOpened) {
|
||||||
|
left = this.isDesktop() ? '256px' : '80px'
|
||||||
|
} else {
|
||||||
|
left = this.isMobile() && '0' || (this.fixSidebar && '80px' || '0')
|
||||||
|
}
|
||||||
|
return left
|
||||||
|
},
|
||||||
|
menuSelect () {
|
||||||
|
if (!this.isDesktop()) {
|
||||||
|
this.collapsed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
233
ui/src/components/page/PageHeader.vue
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-index-wide">
|
||||||
|
<a-breadcrumb class="breadcrumb">
|
||||||
|
<a-breadcrumb-item v-for="(item, index) in breadList" :key="index">
|
||||||
|
<router-link
|
||||||
|
v-if="item.name"
|
||||||
|
:to="{ path: item.path === '' ? '/' : item.path }"
|
||||||
|
>
|
||||||
|
<a-icon v-if="index == 0" :type="item.meta.icon" />
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</router-link>
|
||||||
|
<span v-else-if="$route.params.id">{{ $route.params.id }}</span>
|
||||||
|
<span v-else>{{ item.meta.title }}</span>
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
|
||||||
|
<div class="detail">
|
||||||
|
<div class="main" v-if="!$route.meta.hiddenHeaderContent">
|
||||||
|
<div class="row">
|
||||||
|
<img v-if="logo" :src="logo" class="logo"/>
|
||||||
|
<h1 v-if="title" class="title">{{ title }}</h1>
|
||||||
|
<div class="action">
|
||||||
|
<slot name="action"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<slot name="pageMenu"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Breadcrumb from '@/components/tools/Breadcrumb'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageHeader',
|
||||||
|
components: {
|
||||||
|
's-breadcrumb': Breadcrumb
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
breadcrumb: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
breadList: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getBreadcrumb () {
|
||||||
|
this.breadList = []
|
||||||
|
|
||||||
|
this.name = this.$route.name
|
||||||
|
this.$route.matched.forEach((item) => {
|
||||||
|
// item.name !== 'index' && this.breadList.push(item)
|
||||||
|
this.breadList.push(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route () {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px 32px 0;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
display: flex;
|
||||||
|
/*margin-bottom: 16px;*/
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
flex: 0 1 72px;
|
||||||
|
margin: 0 24px 8px 0;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
border-radius: 72px;
|
||||||
|
display: block;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 100%;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(0,0,0,.85);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
.content, .headerContent {
|
||||||
|
flex: auto;
|
||||||
|
color: rgba(0,0,0,.45);
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
margin-top: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.extra {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
margin-left: 88px;
|
||||||
|
min-width: 242px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.action {
|
||||||
|
margin-left: 56px;
|
||||||
|
min-width: 266px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
text-align: right;
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .page-header {
|
||||||
|
|
||||||
|
.main {
|
||||||
|
|
||||||
|
.row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
flex: 0 1 25%;
|
||||||
|
margin: 0 2% 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content, .headerContent {
|
||||||
|
flex: 0 1 70%;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
margin-top: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin-left: 0;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
margin-left: unset;
|
||||||
|
min-width: 266px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
ui/src/components/page/PageLayout.vue
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div :style="!$route.meta.pageHeader ? 'margin: -24px -24px 0px;' : null">
|
||||||
|
<!-- pageHeader , route meta hideHeader:true on hide -->
|
||||||
|
<page-header v-if="!$route.meta.pageHeader" :title="title" :logo="logo" :avatar="avatar">
|
||||||
|
<slot slot="action" name="action"></slot>
|
||||||
|
<slot slot="content" name="headerContent"></slot>
|
||||||
|
<div slot="content" v-if="!this.$slots.headerContent && desc">
|
||||||
|
<p style="font-size: 14px;color: rgba(0,0,0,.65)">{{ desc }}</p>
|
||||||
|
<div class="link">
|
||||||
|
<template v-for="(link, index) in linkList">
|
||||||
|
<a :key="index" :href="link.href">
|
||||||
|
<a-icon :type="link.icon"/>
|
||||||
|
<span>{{ link.title }}</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot slot="extra" name="extra"></slot>
|
||||||
|
<div slot="pageMenu">
|
||||||
|
<div class="page-menu-search" v-if="search">
|
||||||
|
<a-input-search style="width: 80%; max-width: 522px;" placeholder="请输入..." size="large" enterButton="搜索" />
|
||||||
|
</div>
|
||||||
|
<div class="page-menu-tabs" v-if="tabs && tabs.items">
|
||||||
|
<!-- @change="callback" :activeKey="activeKey" -->
|
||||||
|
<a-tabs :tabBarStyle="{margin: 0}" @change="tabs.callback" :activeKey="tabs.active()">
|
||||||
|
<a-tab-pane v-for="item in tabs.items" :tab="item.title" :key="item.key"></a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</page-header>
|
||||||
|
<div class="content">
|
||||||
|
<div :class="['page-header-index-wide']">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import PageHeader from './PageHeader'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LayoutContent',
|
||||||
|
components: {
|
||||||
|
PageHeader
|
||||||
|
},
|
||||||
|
// ['desc', 'logo', 'title', 'avatar', 'linkList', 'extraImage']
|
||||||
|
props: {
|
||||||
|
desc: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
linkList: {
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
extraImage: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.content {
|
||||||
|
margin: 24px 24px 0;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
&:not(:empty) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
margin-right: 32px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.page-menu-search {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.page-menu-tabs {
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
ui/src/components/page/SHeaderNotice.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<a-popover trigger="click" placement="bottomRight" :overlayStyle="{ width: '300px' }">
|
||||||
|
<template slot="content">
|
||||||
|
<a-spin :spinning="loadding">
|
||||||
|
<a-tabs>
|
||||||
|
<a-tab-pane v-for="(tab, k) in tabs" :tab="tab.title" :key="k">
|
||||||
|
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</a-spin>
|
||||||
|
</template>
|
||||||
|
<span @click="fetchNotice" class="header-notice">
|
||||||
|
<a-badge count="12">
|
||||||
|
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
|
||||||
|
</a-badge>
|
||||||
|
</span>
|
||||||
|
</a-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HeaderNotice',
|
||||||
|
props: {
|
||||||
|
tabs: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loadding: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchNotice () {
|
||||||
|
if (this.loadding) {
|
||||||
|
this.loadding = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loadding = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadding = false
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.header-notice{
|
||||||
|
display: inline-block;
|
||||||
|
transition: all 0.3s;
|
||||||
|
span {
|
||||||
|
vertical-align: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
256
ui/src/components/table/index.js
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import T from 'ant-design-vue/es/table/Table'
|
||||||
|
import get from 'lodash.get'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
needTotalList: [],
|
||||||
|
|
||||||
|
selectedRows: [],
|
||||||
|
selectedRowKeys: [],
|
||||||
|
|
||||||
|
localLoading: false,
|
||||||
|
localDataSource: [],
|
||||||
|
localPagination: Object.assign({}, T.props.pagination)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: Object.assign({}, T.props, {
|
||||||
|
rowKey: {
|
||||||
|
type: [String, Function],
|
||||||
|
default: 'id'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
pageNum: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
showSizeChanger: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'default'
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* {
|
||||||
|
* show: true,
|
||||||
|
* clear: Function
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
alert: {
|
||||||
|
type: [Object, Boolean],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
rowSelection: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
/** @Deprecated */
|
||||||
|
showAlertInfo: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showPagination: {
|
||||||
|
type: String,
|
||||||
|
default: 'auto'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
watch: {
|
||||||
|
'localPagination.current' (val) {
|
||||||
|
this.$router.push({
|
||||||
|
name: this.$route.name,
|
||||||
|
params: Object.assign({}, this.$route.params, {
|
||||||
|
pageNo: val
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pageNum (val) {
|
||||||
|
Object.assign(this.localPagination, {
|
||||||
|
current: val
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pageSize (val) {
|
||||||
|
Object.assign(this.localPagination, {
|
||||||
|
pageSize: val
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showSizeChanger (val) {
|
||||||
|
Object.assign(this.localPagination, {
|
||||||
|
showSizeChanger: val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.localPagination = ['auto', true].includes(this.showPagination) && Object.assign({}, this.localPagination, {
|
||||||
|
current: this.pageNum,
|
||||||
|
pageSize: this.pageSize,
|
||||||
|
showSizeChanger: this.showSizeChanger
|
||||||
|
})
|
||||||
|
this.needTotalList = this.initTotalList(this.columns)
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refresh (bool = false) {
|
||||||
|
bool && (this.localPagination = Object.assign({}, {
|
||||||
|
current: 1, pageSize: this.pageSize
|
||||||
|
}))
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
loadData (pagination, filters, sorter) {
|
||||||
|
this.localLoading = true
|
||||||
|
const parameter = Object.assign({
|
||||||
|
pageNo: (pagination && pagination.current) ||
|
||||||
|
this.localPagination.current,
|
||||||
|
pageSize: (pagination && pagination.pageSize) ||
|
||||||
|
this.localPagination.pageSize
|
||||||
|
},
|
||||||
|
(sorter && sorter.field && {
|
||||||
|
sortField: sorter.field
|
||||||
|
}) || {},
|
||||||
|
(sorter && sorter.order && {
|
||||||
|
sortOrder: sorter.order
|
||||||
|
}) || {}, {
|
||||||
|
...filters
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const result = this.data(parameter)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (result instanceof Promise || '[object Promise]' === result.toString()) {
|
||||||
|
result.then(r => {
|
||||||
|
this.localPagination = Object.assign({}, this.localPagination, {
|
||||||
|
current: r.pageNo, // 返回结果中的当前分页数
|
||||||
|
total: r.totalCount, // 返回结果中的总记录数
|
||||||
|
showSizeChanger: this.showSizeChanger,
|
||||||
|
pageSize: (pagination && pagination.pageSize) ||
|
||||||
|
this.localPagination.pageSize
|
||||||
|
})
|
||||||
|
if (r.data.length === 0 && this.localPagination.current !== 1) {
|
||||||
|
this.localPagination.current--
|
||||||
|
this.loadData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
!r.totalCount && ['auto', false].includes(this.showPagination) && (this.localPagination = false)
|
||||||
|
this.localDataSource = r.data
|
||||||
|
this.localLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initTotalList (columns) {
|
||||||
|
const totalList = []
|
||||||
|
columns && columns instanceof Array && columns.forEach(column => {
|
||||||
|
if (column.needTotal) {
|
||||||
|
totalList.push({
|
||||||
|
...column,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return totalList
|
||||||
|
},
|
||||||
|
updateSelect (selectedRowKeys, selectedRows) {
|
||||||
|
this.selectedRows = selectedRows
|
||||||
|
this.selectedRowKeys = selectedRowKeys
|
||||||
|
const list = this.needTotalList
|
||||||
|
this.needTotalList = list.map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
total: selectedRows.reduce((sum, val) => {
|
||||||
|
const total = sum + parseInt(get(val, item.dataIndex))
|
||||||
|
return isNaN(total) ? 0 : total
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearSelected () {
|
||||||
|
if (this.rowSelection) {
|
||||||
|
this.rowSelection.onChange([], [])
|
||||||
|
this.updateSelect([], [])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderClear (callback) {
|
||||||
|
if (this.selectedRowKeys.length <= 0) return null
|
||||||
|
return (
|
||||||
|
<a style="margin-left: 24px" onClick={() => {
|
||||||
|
callback()
|
||||||
|
this.clearSelected()
|
||||||
|
}}>清空</a>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
renderAlert () {
|
||||||
|
const needTotalItems = this.needTotalList.map((item) => {
|
||||||
|
return (<span style="margin-right: 12px">
|
||||||
|
{item.title}总计 <a style="font-weight: 600">{!item.customRender ? item.total : item.customRender(item.total)}</a>
|
||||||
|
</span>)
|
||||||
|
})
|
||||||
|
|
||||||
|
const clearItem = (typeof this.alert.clear === 'boolean' && this.alert.clear) ? (
|
||||||
|
this.renderClear(this.clearSelected)
|
||||||
|
) : (this.alert !== null && typeof this.alert.clear === 'function') ? (
|
||||||
|
this.renderClear(this.alert.clear)
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a-alert showIcon={true} style="margin-bottom: 16px">
|
||||||
|
<template slot="message">
|
||||||
|
<span style="margin-right: 12px">已选择: <a style="font-weight: 600">{this.selectedRows.length}</a></span>
|
||||||
|
{needTotalItems}
|
||||||
|
{clearItem}
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const props = {}
|
||||||
|
const localKeys = Object.keys(this.$data)
|
||||||
|
const showAlert = (typeof this.alert === 'object' && this.alert !== null && this.alert.show) && typeof this.rowSelection.selectedRowKeys !== 'undefined' || this.alert
|
||||||
|
|
||||||
|
Object.keys(T.props).forEach(k => {
|
||||||
|
const localKey = `local${k.substring(0, 1).toUpperCase()}${k.substring(1)}`
|
||||||
|
if (localKeys.includes(localKey)) {
|
||||||
|
props[k] = this[localKey]
|
||||||
|
return props[k]
|
||||||
|
}
|
||||||
|
if (k === 'rowSelection') {
|
||||||
|
if (showAlert && this.rowSelection) {
|
||||||
|
props[k] = {
|
||||||
|
selectedRows: this.selectedRows,
|
||||||
|
selectedRowKeys: this.selectedRowKeys,
|
||||||
|
onChange: (selectedRowKeys, selectedRows) => {
|
||||||
|
this.updateSelect(selectedRowKeys, selectedRows)
|
||||||
|
typeof this[k].onChange !== 'undefined' && this[k].onChange(selectedRowKeys, selectedRows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props[k]
|
||||||
|
} else if (!this.rowSelection) {
|
||||||
|
props[k] = null
|
||||||
|
return props[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props[k] = this[k]
|
||||||
|
return props[k]
|
||||||
|
})
|
||||||
|
const table = (
|
||||||
|
<a-table {...{ props, scopedSlots: { ...this.$scopedSlots } }} onChange={this.loadData}>
|
||||||
|
{ Object.keys(this.$slots).map(name => (<template slot={name}>{this.$slots[name]}</template>)) }
|
||||||
|
</a-table>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="table-wrapper">
|
||||||
|
{ showAlert ? this.renderAlert() : null }
|
||||||
|
{ table }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
ui/src/components/tools/Breadcrumb.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<a-breadcrumb class="breadcrumb">
|
||||||
|
<a-breadcrumb-item v-for="(item, index) in breadList" :key="index">
|
||||||
|
<router-link v-if="item.name != name" :to="{ path: item.path }">
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</router-link>
|
||||||
|
<span v-else>{{ item.meta.title }}</span>
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
breadList: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getBreadcrumb () {
|
||||||
|
console.log('this.$route.matched', this.$route.matched)
|
||||||
|
|
||||||
|
this.breadList = []
|
||||||
|
this.breadList.push({ name: 'index', path: '/dashboard/', meta: { title: '首页' } })
|
||||||
|
|
||||||
|
this.name = this.$route.name
|
||||||
|
this.$route.matched.forEach((item) => {
|
||||||
|
// item.meta.name === 'dashboard' ? item.path = '/dashboard' : this.$route.path === item.path
|
||||||
|
this.breadList.push(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route () {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
153
ui/src/components/tools/DetailList.vue
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['detail-list', size, layout === 'vertical' ? 'vertical': 'horizontal']">
|
||||||
|
<div v-if="title" class="title">{{ title }}</div>
|
||||||
|
<a-row>
|
||||||
|
<slot></slot>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Col } from 'ant-design-vue/es/grid/'
|
||||||
|
|
||||||
|
const Item = {
|
||||||
|
name: 'DetailListItem',
|
||||||
|
props: {
|
||||||
|
term: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
col: {
|
||||||
|
type: Number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<Col {...{ props: responsive[this.col] }}>
|
||||||
|
<div class="term">{this.$props.term}</div>
|
||||||
|
<div class="content">{this.$slots.default}</div>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsive = {
|
||||||
|
1: { xs: 24 },
|
||||||
|
2: { xs: 24, sm: 12 },
|
||||||
|
3: { xs: 24, sm: 12, md: 8 },
|
||||||
|
4: { xs: 24, sm: 12, md: 6 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DetailList',
|
||||||
|
Item: Item,
|
||||||
|
components: {
|
||||||
|
Col
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
col: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 3
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'large'
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'horizontal'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
col: this.col > 4 ? 4 : this.col
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
.detail-list {
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: rgba(0,0,0,.85);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .term {
|
||||||
|
color: rgba(0,0,0,.85);
|
||||||
|
display: table-cell;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:not(:empty):after {
|
||||||
|
content: ":";
|
||||||
|
margin: 0 8px 0 2px;
|
||||||
|
position: relative;
|
||||||
|
top: -.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .content {
|
||||||
|
color: rgba(0,0,0,.65);
|
||||||
|
display: table-cell;
|
||||||
|
min-height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
width: 100%;
|
||||||
|
&:empty {
|
||||||
|
content: ' ';
|
||||||
|
height: 38px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, .65);
|
||||||
|
font-weight: normal;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
/deep/ .term, .content {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
/deep/ .term, .content {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
.term {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
/deep/ .term, .content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
67
ui/src/components/tools/HeadInfo.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="head-info" :class="center && 'center'">
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
<p>{{ content }}</p>
|
||||||
|
<em v-if="bordered"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HeadInfo',
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
bordered: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.head-info {
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0 32px 0 0;
|
||||||
|
min-width: 125px;
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: rgba(0, 0, 0, .45);
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: rgba(0, 0, 0, .85);
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
em {
|
||||||
|
background-color: #e8e8e8;
|
||||||
|
position: absolute;
|
||||||
|
height: 56px;
|
||||||
|
width: 1px;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
88
ui/src/components/tools/HeaderNotice.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<a-popover
|
||||||
|
v-model="visible"
|
||||||
|
trigger="click"
|
||||||
|
placement="bottomRight"
|
||||||
|
:autoAdjustOverflow="true"
|
||||||
|
:arrowPointAtCenter="true"
|
||||||
|
overlayClassName="header-notice-wrapper"
|
||||||
|
:overlayStyle="{ width: '300px', top: '50px' }">
|
||||||
|
<template slot="content">
|
||||||
|
<a-spin :spinning="loadding">
|
||||||
|
<a-tabs>
|
||||||
|
<a-tab-pane tab="通知" key="1">
|
||||||
|
<a-list>
|
||||||
|
<a-list-item>
|
||||||
|
<a-list-item-meta title="你收到了 14 份新周报" description="一年前">
|
||||||
|
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
|
||||||
|
</a-list-item-meta>
|
||||||
|
</a-list-item>
|
||||||
|
<a-list-item>
|
||||||
|
<a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
|
||||||
|
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
|
||||||
|
</a-list-item-meta>
|
||||||
|
</a-list-item>
|
||||||
|
<a-list-item>
|
||||||
|
<a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
|
||||||
|
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
|
||||||
|
</a-list-item-meta>
|
||||||
|
</a-list-item>
|
||||||
|
</a-list>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane tab="消息" key="2">
|
||||||
|
123
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane tab="待办" key="3">
|
||||||
|
123
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</a-spin>
|
||||||
|
</template>
|
||||||
|
<span @click="fetchNotice" class="header-notice">
|
||||||
|
<a-badge count="12">
|
||||||
|
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
|
||||||
|
</a-badge>
|
||||||
|
</span>
|
||||||
|
</a-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HeaderNotice',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loadding: false,
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchNotice () {
|
||||||
|
if (!this.visible) {
|
||||||
|
this.loadding = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadding = false
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
this.loadding = false
|
||||||
|
}
|
||||||
|
this.visible = !this.visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
.header-notice-wrapper {
|
||||||
|
top: 50px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.header-notice{
|
||||||
|
display: inline-block;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
span {
|
||||||
|
vertical-align: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
ui/src/components/tools/Logo.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="logo">
|
||||||
|
<img src="../../assets/logo.png"/>
|
||||||
|
<project-menu></project-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ProjectMenu from '../tools/ProjectMenu'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Logo',
|
||||||
|
components: {
|
||||||
|
ProjectMenu
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'CloudStack',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
showTitle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.logo img {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
73
ui/src/components/tools/ProjectMenu.vue
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<span class="project-wrapper" :disabled="true">
|
||||||
|
<a-select
|
||||||
|
size="default"
|
||||||
|
style="width: 75%"
|
||||||
|
defaultValue="Default View"
|
||||||
|
:value="selectedProject"
|
||||||
|
:disabled="isDisabled()"
|
||||||
|
:filterOption="filterProject"
|
||||||
|
@change="changeProject"
|
||||||
|
showSearch>
|
||||||
|
<a-select-option v-for="(project, index) in projects" :key="index">
|
||||||
|
{{ project.name }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue'
|
||||||
|
import { api } from '@/api'
|
||||||
|
import store from '@/store'
|
||||||
|
import { CURRENT_PROJECT } from '@/store/mutation-types'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ProjectMenu',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
projects: [],
|
||||||
|
selectedProject: 'Default View'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
if (this.isDisabled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api('listProjects').then(json => {
|
||||||
|
this.projects = [{ name: 'Default View' }]
|
||||||
|
if (json && json.listprojectsresponse && json.listprojectsresponse.project) {
|
||||||
|
this.projects.push(...json.listprojectsresponse.project)
|
||||||
|
}
|
||||||
|
const currentProject = Vue.ls.get(CURRENT_PROJECT)
|
||||||
|
for (var project of this.projects) {
|
||||||
|
if (project.id === currentProject.id) {
|
||||||
|
this.selectedProject = project.name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
isDisabled () {
|
||||||
|
return !store.getters.apis.hasOwnProperty('listProjects')
|
||||||
|
},
|
||||||
|
changeProject (index) {
|
||||||
|
const project = this.projects[index]
|
||||||
|
this.selectedProject = project.name
|
||||||
|
this.$store.dispatch('SetProject', project)
|
||||||
|
this.$store.dispatch('ToggleTheme', project.id === undefined ? 'light' : 'dark')
|
||||||
|
this.$message.success(`Switched to "${project.name}"`)
|
||||||
|
this.$router.push({ name: 'dashboard' })
|
||||||
|
},
|
||||||
|
filterProject (input, option) {
|
||||||
|
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
46
ui/src/components/tools/TranslationMenu.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<a-dropdown>
|
||||||
|
<span class="action ant-dropdown-link">
|
||||||
|
<a-icon type="global"></a-icon>
|
||||||
|
</span>
|
||||||
|
<a-menu slot="overlay" @click="onClick">
|
||||||
|
<a-menu-item key="en" :value="enUS">English</a-menu-item>
|
||||||
|
<a-menu-item key="cn" :value="null">Chinese</a-menu-item>
|
||||||
|
<a-menu-item key="jp" :value="null">Japanese</a-menu-item>
|
||||||
|
<a-menu-item key="fr" :value="null">French</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-dropdown>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import enUS from 'ant-design-vue/lib/locale-provider/en_US'
|
||||||
|
import moment from 'moment'
|
||||||
|
import 'moment/locale/zh-cn'
|
||||||
|
moment.locale('en')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TranslationMenu',
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
locale: null,
|
||||||
|
enUS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
moment,
|
||||||
|
onClick (e) {
|
||||||
|
const localeValue = e.target.value
|
||||||
|
this.locale = localeValue
|
||||||
|
if (!localeValue) {
|
||||||
|
moment.locale('en')
|
||||||
|
} else {
|
||||||
|
moment.locale('zh-cn')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
89
ui/src/components/tools/TwoStepCaptcha.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 两步验证 -->
|
||||||
|
<a-modal
|
||||||
|
centered
|
||||||
|
v-model="visible"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
:maskClosable="false"
|
||||||
|
>
|
||||||
|
<div slot="title" :style="{ textAlign: 'center' }">两步验证</div>
|
||||||
|
<template slot="footer">
|
||||||
|
<div :style="{ textAlign: 'center' }">
|
||||||
|
<a-button key="back" @click="handleCancel">返回</a-button>
|
||||||
|
<a-button key="submit" type="primary" :loading="stepLoading" @click="handleStepOk">
|
||||||
|
继续
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-spin :spinning="stepLoading">
|
||||||
|
<a-form layout="vertical" :auto-form-create="(form)=>{this.form = form}">
|
||||||
|
<div class="step-form-wrapper">
|
||||||
|
<p style="text-align: center" v-if="!stepLoading">请在手机中打开 Google Authenticator 或两步验证 APP<br />输入 6 位动态码</p>
|
||||||
|
<p style="text-align: center" v-else>正在验证..<br/>请稍后</p>
|
||||||
|
<a-form-item
|
||||||
|
:style="{ textAlign: 'center' }"
|
||||||
|
hasFeedback
|
||||||
|
fieldDecoratorId="stepCode"
|
||||||
|
:fieldDecoratorOptions="{rules: [{ required: true, message: '请输入 6 位动态码!', pattern: /^\d{6}$/, len: 6 }]}"
|
||||||
|
>
|
||||||
|
<a-input :style="{ textAlign: 'center' }" @keyup.enter.native="handleStepOk" placeholder="000000" />
|
||||||
|
</a-form-item>
|
||||||
|
<p style="text-align: center">
|
||||||
|
<a @click="onForgeStepCode">遗失手机?</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-spin>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
stepLoading: false,
|
||||||
|
|
||||||
|
form: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleStepOk () {
|
||||||
|
const vm = this
|
||||||
|
this.stepLoading = true
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log('values', values)
|
||||||
|
setTimeout(() => {
|
||||||
|
vm.stepLoading = false
|
||||||
|
vm.$emit('success', { values })
|
||||||
|
}, 2000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.stepLoading = false
|
||||||
|
this.$emit('error', { err })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleCancel () {
|
||||||
|
this.visible = false
|
||||||
|
this.$emit('cancel')
|
||||||
|
},
|
||||||
|
onForgeStepCode () {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.step-form-wrapper {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
78
ui/src/components/tools/UserMenu.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-wrapper">
|
||||||
|
|
||||||
|
<translation-menu class="action"/>
|
||||||
|
<header-notice class="action"/>
|
||||||
|
<a-dropdown>
|
||||||
|
<span class="action ant-dropdown-link user-dropdown-menu">
|
||||||
|
<a-avatar class="avatar" size="small" :src="avatar()"/>
|
||||||
|
<span>{{ nickname() }}</span>
|
||||||
|
</span>
|
||||||
|
<a-menu slot="overlay" class="user-dropdown-menu-wrapper">
|
||||||
|
<a-menu-item key="0">
|
||||||
|
<router-link :to="{ name: 'account' }">
|
||||||
|
<a-icon type="user"/>
|
||||||
|
<span>Profile</span>
|
||||||
|
</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="1">
|
||||||
|
<router-link :to="{ name: 'account' }">
|
||||||
|
<a-icon type="setting"/>
|
||||||
|
<span>Account</span>
|
||||||
|
</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="2" disabled>
|
||||||
|
<a-icon type="setting"/>
|
||||||
|
<span>测试</span>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="2" disabled>
|
||||||
|
<a :href="helpUrl" target="_blank">
|
||||||
|
<a-icon type="question-circle-o"></a-icon>
|
||||||
|
<span>Help</span>
|
||||||
|
</a>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-divider/>
|
||||||
|
<a-menu-item key="3">
|
||||||
|
<a href="javascript:;" @click="handleLogout">
|
||||||
|
<a-icon type="logout"/>
|
||||||
|
<span>Logout</span>
|
||||||
|
</a>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import config from '@/config/defaultSettings'
|
||||||
|
import HeaderNotice from './HeaderNotice'
|
||||||
|
import TranslationMenu from './TranslationMenu'
|
||||||
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UserMenu',
|
||||||
|
components: {
|
||||||
|
TranslationMenu,
|
||||||
|
HeaderNotice
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
helpUrl: config.helpUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['Logout']),
|
||||||
|
...mapGetters(['nickname', 'avatar']),
|
||||||
|
handleLogout () {
|
||||||
|
return this.Logout({}).then(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}).catch(err => {
|
||||||
|
this.$message.error({
|
||||||
|
title: '错误',
|
||||||
|
description: err.message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
95
ui/src/components/tools/setting.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { message } from 'ant-design-vue/es'
|
||||||
|
// import defaultSettings from '../defaultSettings';
|
||||||
|
|
||||||
|
let lessNodesAppended
|
||||||
|
|
||||||
|
const colorList = [
|
||||||
|
{
|
||||||
|
key: '薄暮', color: '#F5222D'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '火山', color: '#FA541C'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '日暮', color: '#FAAD14'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '明青', color: '#13C2C2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '极光绿', color: '#52C41A'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '拂晓蓝(默认)', color: '#1890FF'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '极客蓝', color: '#2F54EB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '酱紫', color: '#722ED1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const updateTheme = primaryColor => {
|
||||||
|
// Don't compile less in production!
|
||||||
|
/* if (process.env.NODE_ENV === 'production') {
|
||||||
|
return;
|
||||||
|
} */
|
||||||
|
// Determine if the component is remounted
|
||||||
|
if (!primaryColor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const hideMessage = message.loading('正在编译主题!', 0)
|
||||||
|
function buildIt () {
|
||||||
|
if (!window.less) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
window.less
|
||||||
|
.modifyVars({
|
||||||
|
'@primary-color': primaryColor
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
hideMessage()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('Failed to update theme')
|
||||||
|
hideMessage()
|
||||||
|
})
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
if (!lessNodesAppended) {
|
||||||
|
// insert less.js and color.less
|
||||||
|
const lessStyleNode = document.createElement('link')
|
||||||
|
const lessConfigNode = document.createElement('script')
|
||||||
|
const lessScriptNode = document.createElement('script')
|
||||||
|
lessStyleNode.setAttribute('rel', 'stylesheet/less')
|
||||||
|
lessStyleNode.setAttribute('href', '/color.less')
|
||||||
|
lessConfigNode.innerHTML = `
|
||||||
|
window.less = {
|
||||||
|
async: true,
|
||||||
|
env: 'production',
|
||||||
|
javascriptEnabled: true
|
||||||
|
};
|
||||||
|
`
|
||||||
|
lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'
|
||||||
|
lessScriptNode.async = true
|
||||||
|
lessScriptNode.onload = () => {
|
||||||
|
buildIt()
|
||||||
|
lessScriptNode.onload = null
|
||||||
|
}
|
||||||
|
document.body.appendChild(lessStyleNode)
|
||||||
|
document.body.appendChild(lessConfigNode)
|
||||||
|
document.body.appendChild(lessScriptNode)
|
||||||
|
lessNodesAppended = true
|
||||||
|
} else {
|
||||||
|
buildIt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateColorWeak = colorWeak => {
|
||||||
|
// document.body.className = colorWeak ? 'colorWeak' : '';
|
||||||
|
colorWeak ? document.body.classList.add('colorWeak') : document.body.classList.remove('colorWeak')
|
||||||
|
}
|
||||||
|
|
||||||
|
export { updateTheme, colorList, updateColorWeak }
|
||||||
148
ui/src/config/apiConfig.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// section -> list apis and actions
|
||||||
|
|
||||||
|
export const apiConfig = {
|
||||||
|
|
||||||
|
// Instance
|
||||||
|
'vm': {
|
||||||
|
icon: 'cloud',
|
||||||
|
path: 'vm',
|
||||||
|
label: 'Instances',
|
||||||
|
listApi: 'listVirtualMachinesMetrics',
|
||||||
|
column: ['name', 'instancename', 'state', 'nic[].ipaddress', 'zonename', 'account', 'domain',
|
||||||
|
'cpunumber', 'cpuused', 'cputotal', 'memoryintfreekbs', 'memorytotal', 'networkread', 'networkwrite', 'diskkbsread', 'diskkbswrite', 'diskiopstotal'
|
||||||
|
],
|
||||||
|
hidden: ['zonename', 'account', 'domain'],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
api: 'deployVirtualMachine',
|
||||||
|
icon: 'plus',
|
||||||
|
label: 'Deploy VM',
|
||||||
|
type: 'main',
|
||||||
|
params: ['name', 'zoneid', 'diskofferingid']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Storage Group
|
||||||
|
'storage': {
|
||||||
|
icon: 'database',
|
||||||
|
path: 'storage',
|
||||||
|
label: 'Storage',
|
||||||
|
redirect: 'volume',
|
||||||
|
children: [
|
||||||
|
'volume',
|
||||||
|
'snapshot',
|
||||||
|
'vmsnapshot'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Volume
|
||||||
|
'volume': {
|
||||||
|
icon: 'hdd',
|
||||||
|
path: 'volume',
|
||||||
|
label: 'Volumes',
|
||||||
|
parent: 'storage',
|
||||||
|
listApi: 'listVolumesMetrics',
|
||||||
|
column: ['name', 'state', 'type', 'vmname', 'size', 'physicalsize', 'utilization', 'storage', 'hypervisor', 'account', 'domain', 'zonename'],
|
||||||
|
hidden: ['storage', 'utilization'],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
api: 'createVolume',
|
||||||
|
icon: 'plus',
|
||||||
|
label: 'Create Volumes',
|
||||||
|
type: 'main',
|
||||||
|
params: ['name', 'zoneid', 'diskofferingid']
|
||||||
|
}, {
|
||||||
|
api: 'uploadVolume',
|
||||||
|
icon: 'cloud-upload',
|
||||||
|
label: 'Upload Volume From URL',
|
||||||
|
type: 'main',
|
||||||
|
params: ['url', 'name', 'zoneid', 'format', 'diskofferingid', 'checksum']
|
||||||
|
}, {
|
||||||
|
api: 'getUploadParamsForVolume',
|
||||||
|
icon: 'upload',
|
||||||
|
label: 'Upload Local Volume',
|
||||||
|
params: ['@file', 'name', 'zoneid', 'format', 'checksum']
|
||||||
|
}, {
|
||||||
|
api: 'resizeVolume',
|
||||||
|
icon: 'fullscreen',
|
||||||
|
label: 'Resize Volume',
|
||||||
|
type: 'main',
|
||||||
|
params: ['id', 'virtualmachineid']
|
||||||
|
}, {
|
||||||
|
api: 'attachVolume',
|
||||||
|
icon: 'paper-clip',
|
||||||
|
label: 'Attach Volume',
|
||||||
|
params: ['id', 'virtualmachineid']
|
||||||
|
}, {
|
||||||
|
api: 'detachVolume',
|
||||||
|
icon: 'link',
|
||||||
|
label: 'Detach Volume',
|
||||||
|
params: ['id', 'virtualmachineid']
|
||||||
|
}, {
|
||||||
|
api: 'extractVolume',
|
||||||
|
icon: 'cloud-download',
|
||||||
|
label: 'Download Volume',
|
||||||
|
params: ['id', 'zoneid', 'mode'],
|
||||||
|
paramOptions: {
|
||||||
|
'mode': {
|
||||||
|
'value': 'HTTP_DOWNLOAD'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
api: 'migrateVolume',
|
||||||
|
icon: 'drag',
|
||||||
|
label: 'Migrate Volume',
|
||||||
|
params: ['volumeid', 'storageid', 'livemigrate']
|
||||||
|
}, {
|
||||||
|
api: 'deleteVolume',
|
||||||
|
icon: 'delete',
|
||||||
|
label: 'Delete Volume',
|
||||||
|
params: ['id']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Snapshot Tab
|
||||||
|
'snapshot': {
|
||||||
|
icon: 'build',
|
||||||
|
path: 'snapshot',
|
||||||
|
label: 'Snapshots',
|
||||||
|
parent: 'storage',
|
||||||
|
listApi: 'listSnapshots',
|
||||||
|
column: ['volumename', 'name', 'state', 'intervaltype', 'created', 'account', 'domain'],
|
||||||
|
hidden: [],
|
||||||
|
actions: [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// VM Snapshot Tab
|
||||||
|
'vmsnapshot': {
|
||||||
|
icon: 'camera',
|
||||||
|
path: 'vmsnapshot',
|
||||||
|
label: 'VM Snapshots',
|
||||||
|
parent: 'storage',
|
||||||
|
listApi: 'listVMSnapshot',
|
||||||
|
column: ['name', 'state', 'type', 'current', 'parent', 'created', 'account', 'domain'],
|
||||||
|
hidden: ['storage'],
|
||||||
|
actions: [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Guest Network
|
||||||
|
'guestnetwork': {
|
||||||
|
icon: 'wifi',
|
||||||
|
listApi: 'listNetworks',
|
||||||
|
column: [],
|
||||||
|
hidden: [],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
api: 'deleteNetwork',
|
||||||
|
icon: 'delete',
|
||||||
|
label: 'Delete Network',
|
||||||
|
params: ['id']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
ui/src/config/defaultSettings.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export default {
|
||||||
|
primaryColor: '#39A7DE', // primary color of ant design
|
||||||
|
navTheme: 'light', // theme for nav menu
|
||||||
|
layout: 'sidemenu', // nav menu position: sidemenu or topmenu
|
||||||
|
contentWidth: 'Fixed', // layout of content: Fluid or Fixed, only works when layout is topmenu
|
||||||
|
fixedHeader: true, // sticky header
|
||||||
|
fixSiderbar: true, // sticky siderbar
|
||||||
|
autoHideHeader: false, // auto hide header
|
||||||
|
colorWeak: false,
|
||||||
|
multiTab: false, // enable to have tab/route history stuff
|
||||||
|
// CloudStack options
|
||||||
|
apiBase: '/client/api',
|
||||||
|
helpUrl: 'http://docs.cloudstack.apache.org',
|
||||||
|
appTitle: 'CloudStack',
|
||||||
|
// vue-ls options
|
||||||
|
storageOptions: {
|
||||||
|
namespace: 'cs__', // key prefix
|
||||||
|
name: 'ls', // name variable Vue.[ls] or this.[$ls],
|
||||||
|
storage: 'local' // storage name session, local, memory
|
||||||
|
}
|
||||||
|
}
|
||||||
663
ui/src/config/router.config.js
Normal file
@ -0,0 +1,663 @@
|
|||||||
|
// eslint-disable-next-line
|
||||||
|
import { UserLayout, BasicLayout, RouteView, BlankLayout, PageView } from '@/components/layouts'
|
||||||
|
|
||||||
|
export const asyncRouterMap = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'index',
|
||||||
|
component: BasicLayout,
|
||||||
|
meta: { icon: 'home' },
|
||||||
|
redirect: '/dashboard',
|
||||||
|
children: [
|
||||||
|
// dashboard
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
name: 'dashboard',
|
||||||
|
meta: { title: 'Dashboard', keepAlive: true, icon: 'dashboard' },
|
||||||
|
component: () => import('@/views/dashboard/Dashboard')
|
||||||
|
},
|
||||||
|
|
||||||
|
// instance
|
||||||
|
{
|
||||||
|
path: '/vm',
|
||||||
|
name: 'vm',
|
||||||
|
meta: { title: 'Instances', keepAlive: true, icon: 'cloud', permission: [ 'listVirtualMachinesMetrics', 'listVirtualMachines' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'vm',
|
||||||
|
path: '/vm/:id',
|
||||||
|
meta: { title: 'Instances', keepAlive: true, icon: 'cloud', permission: [ 'listVirtualMachinesMetrics', 'listVirtualMachines' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// storage
|
||||||
|
{
|
||||||
|
path: '/storage',
|
||||||
|
name: 'storage',
|
||||||
|
meta: { title: 'Storage', keepAlive: true, icon: 'database', permission: [ 'listVolumesMetrics', 'listVolumes' ] },
|
||||||
|
component: RouteView,
|
||||||
|
redirect: '/volume',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/volume',
|
||||||
|
name: 'volume',
|
||||||
|
meta: { title: 'Volumes', icon: 'hdd', permission: [ 'listVolumesMetrics', 'listVolumes' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'volume',
|
||||||
|
path: '/volume/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/snapshot',
|
||||||
|
name: 'snapshot',
|
||||||
|
meta: { title: 'Snapshots', icon: 'build', permission: [ 'listSnapshots' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'snapshot',
|
||||||
|
path: '/snapshot/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/vmsnapshot',
|
||||||
|
name: 'vmsnapshot',
|
||||||
|
meta: { title: 'VM Snapshots', icon: 'camera', permission: [ 'listVMSnapshot' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'vmsnapshot',
|
||||||
|
path: '/vmsnapshot/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// network
|
||||||
|
{
|
||||||
|
path: '/network',
|
||||||
|
name: 'network',
|
||||||
|
meta: { title: 'Network', keepAlive: true, icon: 'wifi', permission: [ 'listNetworks' ] },
|
||||||
|
component: RouteView,
|
||||||
|
redirect: '/guestnetwork',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/guestnetwork',
|
||||||
|
name: 'guestnetwork',
|
||||||
|
meta: { title: 'Guest Networks', icon: 'gateway', permission: [ 'listNetworks' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'guestnetwork',
|
||||||
|
path: '/guestnetwork/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/securitygroups',
|
||||||
|
name: 'securitygroups',
|
||||||
|
meta: { title: 'Security Groups', icon: 'compass', permission: [ 'listSecurityGroups' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'securitygroups',
|
||||||
|
path: '/securitygroups/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/vpc',
|
||||||
|
name: 'vpc',
|
||||||
|
meta: { title: 'VPCs', icon: 'deployment-unit', permission: [ 'listVPCs' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'vpc',
|
||||||
|
path: '/vpc/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/vpngateway',
|
||||||
|
name: 'vpngateway',
|
||||||
|
meta: { title: 'VPN Gateways', icon: 'lock', permission: [ 'listVpnCustomerGateways' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'vpngateway',
|
||||||
|
path: '/vpngateway/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// image
|
||||||
|
{
|
||||||
|
path: '/image',
|
||||||
|
name: 'image',
|
||||||
|
meta: { title: 'Images', keepAlive: true, icon: 'picture', permission: [ 'listTemplates' ] },
|
||||||
|
component: RouteView,
|
||||||
|
redirect: '/template',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/template',
|
||||||
|
name: 'template',
|
||||||
|
meta: { title: 'Templates', icon: 'save', permission: [ 'listTemplates' ], params: { 'templatefilter': 'executable' } },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
path: '/template/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/iso',
|
||||||
|
name: 'iso',
|
||||||
|
meta: { title: 'Isos', icon: 'usb', permission: [ 'listIsos' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'iso',
|
||||||
|
path: '/iso/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// audit
|
||||||
|
{
|
||||||
|
path: '/audit',
|
||||||
|
name: 'audit',
|
||||||
|
meta: { title: 'Audit', keepAlive: true, icon: 'audit', permission: [ 'listEvents', 'listAlerts' ] },
|
||||||
|
component: RouteView,
|
||||||
|
redirect: '/events',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/events',
|
||||||
|
name: 'events',
|
||||||
|
meta: { title: 'Events', icon: 'bars', permission: [ 'listEvents' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/events/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/alerts',
|
||||||
|
name: 'alerts',
|
||||||
|
meta: { title: 'Alerts', icon: 'sound', permission: [ 'listAlerts' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/alerts/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// project
|
||||||
|
{
|
||||||
|
path: '/project',
|
||||||
|
name: 'project',
|
||||||
|
meta: { title: 'Projects', icon: 'project', permission: [ 'listProjects' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/project/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// org
|
||||||
|
{
|
||||||
|
path: '/manage',
|
||||||
|
name: 'manage',
|
||||||
|
meta: { title: 'Manage', keepAlive: true, icon: 'solution', permission: [ 'listAccounts' ] },
|
||||||
|
component: RouteView,
|
||||||
|
redirect: '/account',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/ssh',
|
||||||
|
name: 'ssh',
|
||||||
|
meta: { title: 'SSH Keys', icon: 'safety', permission: [ 'listSSHKeyPairs' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/ssh/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/affinitygroups',
|
||||||
|
name: 'affinitygroups',
|
||||||
|
meta: { title: 'Affinity Groups', icon: 'rocket', permission: [ 'listAffinityGroups' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/affinitygroups/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
name: 'user',
|
||||||
|
meta: { title: 'Users', icon: 'user', permission: [ 'listUsers' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/user/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/account',
|
||||||
|
name: 'account',
|
||||||
|
meta: { title: 'Accounts', icon: 'team', permission: [ 'listAccounts' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/account/:id',
|
||||||
|
meta: { title: 'Manage', keepAlive: true, icon: 'solution', permission: [ 'listAccounts' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/domain',
|
||||||
|
name: 'domain',
|
||||||
|
meta: { title: 'Domains', icon: 'block', permission: [ 'listDomains' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/domain/:id',
|
||||||
|
meta: { title: 'Domains', icon: 'block', permission: [ 'listDomains' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/role',
|
||||||
|
name: 'role',
|
||||||
|
meta: { title: 'Roles', icon: 'idcard', permission: [ 'listRoles' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/role/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// infra
|
||||||
|
{
|
||||||
|
path: '/infra',
|
||||||
|
name: 'infra',
|
||||||
|
meta: { title: 'Infrastructure', keepAlive: true, icon: 'appstore', permission: [ 'listInfrastructure' ] },
|
||||||
|
component: RouteView,
|
||||||
|
redirect: '/zone',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/zone',
|
||||||
|
name: 'zone',
|
||||||
|
meta: { title: 'Zones', icon: 'table', permission: [ 'listZonesMetrics', 'listZones' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/zone/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/pod',
|
||||||
|
name: 'pod',
|
||||||
|
meta: { title: 'Pods', icon: 'hdd', permission: [ 'listPods' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/pod/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/cluster',
|
||||||
|
name: 'cluster',
|
||||||
|
meta: { title: 'Clusters', icon: 'cluster', permission: [ 'listClustersMetrics', 'listClusters' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/cluster/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/host',
|
||||||
|
name: 'host',
|
||||||
|
meta: { title: 'Hosts', icon: 'desktop', permission: [ 'listHostsMetrics', 'listHosts' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/host/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/primarystorage',
|
||||||
|
name: 'primarystorage',
|
||||||
|
meta: { title: 'Primary Storage', icon: 'gold', permission: [ 'listStoragePoolsMetrics', 'listStoragePools' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/primarystorage/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/secondarystorage',
|
||||||
|
name: 'secondarystorage',
|
||||||
|
meta: { title: 'Secondary Storage', icon: 'switcher', permission: [ 'listImageStores' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/secondarystorage/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/systemvm',
|
||||||
|
name: 'systemvm',
|
||||||
|
meta: { title: 'System VMs', icon: 'thunderbolt', permission: [ 'listSystemVms' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/systemvm/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/router',
|
||||||
|
name: 'router',
|
||||||
|
meta: { title: 'Virtual Routers', icon: 'fork', permission: [ 'listRouters' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/router/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/cpusockets',
|
||||||
|
name: 'cpusocket',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
meta: { title: 'CPU Sockets', icon: 'api', permission: [ 'listHosts' ] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// offerings
|
||||||
|
{
|
||||||
|
path: '/offering',
|
||||||
|
name: 'Offerings',
|
||||||
|
meta: { title: 'Offerings', keepAlive: true, icon: 'layout', permission: [ 'listServiceOfferings' ] },
|
||||||
|
component: RouteView,
|
||||||
|
redirect: '/computeoffering',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/computeoffering',
|
||||||
|
name: 'computeoffering',
|
||||||
|
meta: { title: 'Compute Offerings', icon: 'cloud', permission: [ 'listServiceOfferings' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/computeoffering/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/diskoffering',
|
||||||
|
name: 'diskoffering',
|
||||||
|
meta: { title: 'Disk Offerings', icon: 'save', permission: [ 'listDiskOfferings' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/diskoffering/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/networkoffering',
|
||||||
|
name: 'networkoffering',
|
||||||
|
meta: { title: 'Network Offerings', icon: 'wifi', permission: [ 'listNetworkOfferings' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/networkoffering/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/vpcoffering',
|
||||||
|
name: 'vpcoffering',
|
||||||
|
meta: { title: 'VPC Offerings', icon: 'deployment-unit', permission: [ 'listVPCOfferings' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/vpcoffering/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/systemoffering',
|
||||||
|
name: 'systemoffering',
|
||||||
|
meta: { title: 'System Offerings', icon: 'setting', permission: [ 'listServiceOfferings' ], params: { 'issystem': 'true' } },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/systemoffering/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// setting
|
||||||
|
{
|
||||||
|
path: '/setting',
|
||||||
|
name: 'Settings',
|
||||||
|
meta: { title: 'Settings', keepAlive: true, icon: 'setting', permission: [ 'listConfigurations' ] },
|
||||||
|
component: RouteView,
|
||||||
|
redirect: '/globalsetting',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/globalsetting',
|
||||||
|
name: 'globalsetting',
|
||||||
|
meta: { title: 'Global Settings', icon: 'global', permission: [ 'listConfigurations' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/globalsetting/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ldapsetting',
|
||||||
|
name: 'ldapsetting',
|
||||||
|
meta: { title: 'LDAP Settings', icon: 'team', permission: [ 'listLdapConfigurations' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/ldapsetting/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/hypervisorcapability',
|
||||||
|
name: 'hypervisorcapability',
|
||||||
|
meta: { title: 'Hypervisor Capabilities', icon: 'database', permission: [ 'listHypervisorCapabilities' ] },
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue'),
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/hypervisorcapability/:id',
|
||||||
|
component: () => import('@/components/CloudMonkey/Resource.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Exceptions
|
||||||
|
{
|
||||||
|
path: '/exception',
|
||||||
|
name: 'exception',
|
||||||
|
component: RouteView,
|
||||||
|
hidden: true,
|
||||||
|
redirect: '/exception/403',
|
||||||
|
meta: { title: 'Exception', icon: 'warning' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/exception/403',
|
||||||
|
name: 'Exception403',
|
||||||
|
hidden: true,
|
||||||
|
component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/403'),
|
||||||
|
meta: { title: '403' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/exception/404',
|
||||||
|
name: 'Exception404',
|
||||||
|
hidden: true,
|
||||||
|
component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/404'),
|
||||||
|
meta: { title: '404' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/exception/500',
|
||||||
|
name: 'Exception500',
|
||||||
|
hidden: true,
|
||||||
|
component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/500'),
|
||||||
|
meta: { title: '500' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*', redirect: '/exception/404', hidden: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const constantRouterMap = [
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
component: UserLayout,
|
||||||
|
redirect: '/user/login',
|
||||||
|
hidden: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import(/* webpackChunkName: "user" */ '@/views/user/Login')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test',
|
||||||
|
component: BlankLayout,
|
||||||
|
redirect: '/test/home',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'home',
|
||||||
|
name: 'TestHome',
|
||||||
|
component: () => import('@/views/Home')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/403',
|
||||||
|
component: () => import(/* webpackChunkName: "forbidden" */ '@/views/exception/403')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/404',
|
||||||
|
component: () => import(/* webpackChunkName: "fail" */ '@/views/exception/404')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/500',
|
||||||
|
component: () => import(/* webpackChunkName: "error" */ '@/views/exception/500')
|
||||||
|
}
|
||||||
|
]
|
||||||
34
ui/src/core/bootstrap.js
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import store from '@/store/'
|
||||||
|
import {
|
||||||
|
ACCESS_TOKEN,
|
||||||
|
CURRENT_PROJECT,
|
||||||
|
DEFAULT_COLOR,
|
||||||
|
DEFAULT_THEME,
|
||||||
|
DEFAULT_LAYOUT_MODE,
|
||||||
|
DEFAULT_COLOR_WEAK,
|
||||||
|
SIDEBAR_TYPE,
|
||||||
|
DEFAULT_FIXED_HEADER,
|
||||||
|
DEFAULT_FIXED_HEADER_HIDDEN,
|
||||||
|
DEFAULT_FIXED_SIDEMENU,
|
||||||
|
DEFAULT_CONTENT_WIDTH_TYPE,
|
||||||
|
DEFAULT_MULTI_TAB
|
||||||
|
} from '@/store/mutation-types'
|
||||||
|
import config from '@/config/defaultSettings'
|
||||||
|
|
||||||
|
export default function Initializer () {
|
||||||
|
store.commit('SET_SIDEBAR_TYPE', Vue.ls.get(SIDEBAR_TYPE, true))
|
||||||
|
store.commit('TOGGLE_THEME', Vue.ls.get(DEFAULT_THEME, config.navTheme))
|
||||||
|
store.commit('TOGGLE_LAYOUT_MODE', Vue.ls.get(DEFAULT_LAYOUT_MODE, config.layout))
|
||||||
|
store.commit('TOGGLE_FIXED_HEADER', Vue.ls.get(DEFAULT_FIXED_HEADER, config.fixedHeader))
|
||||||
|
store.commit('TOGGLE_FIXED_SIDERBAR', Vue.ls.get(DEFAULT_FIXED_SIDEMENU, config.fixSiderbar))
|
||||||
|
store.commit('TOGGLE_CONTENT_WIDTH', Vue.ls.get(DEFAULT_CONTENT_WIDTH_TYPE, config.contentWidth))
|
||||||
|
store.commit('TOGGLE_FIXED_HEADER_HIDDEN', Vue.ls.get(DEFAULT_FIXED_HEADER_HIDDEN, config.autoHideHeader))
|
||||||
|
store.commit('TOGGLE_WEAK', Vue.ls.get(DEFAULT_COLOR_WEAK, config.colorWeak))
|
||||||
|
store.commit('TOGGLE_COLOR', Vue.ls.get(DEFAULT_COLOR, config.primaryColor))
|
||||||
|
store.commit('TOGGLE_MULTI_TAB', Vue.ls.get(DEFAULT_MULTI_TAB, config.multiTab))
|
||||||
|
store.commit('SET_TOKEN', Vue.ls.get(ACCESS_TOKEN))
|
||||||
|
store.commit('SET_PROJECT', Vue.ls.get(CURRENT_PROJECT))
|
||||||
|
|
||||||
|
// last step
|
||||||
|
}
|
||||||
3
ui/src/core/icons.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import bxAnaalyse from '@/assets/icons/bx-analyse.svg?inline' // path to your '*.svg?inline' file.
|
||||||
|
|
||||||
|
export { bxAnaalyse }
|
||||||
99
ui/src/core/lazy_lib/components_use.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* 该文件是为了按需加载,剔除掉了一些不需要的框架组件。
|
||||||
|
* 减少了编译支持库包大小
|
||||||
|
*
|
||||||
|
* 当需要更多组件依赖时,在该文件加入即可
|
||||||
|
*/
|
||||||
|
import Vue from 'vue'
|
||||||
|
import {
|
||||||
|
LocaleProvider,
|
||||||
|
Layout,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Button,
|
||||||
|
Switch,
|
||||||
|
Radio,
|
||||||
|
Checkbox,
|
||||||
|
Select,
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
Tabs,
|
||||||
|
Icon,
|
||||||
|
Badge,
|
||||||
|
Popover,
|
||||||
|
Dropdown,
|
||||||
|
List,
|
||||||
|
Avatar,
|
||||||
|
Breadcrumb,
|
||||||
|
Steps,
|
||||||
|
Spin,
|
||||||
|
Menu,
|
||||||
|
Drawer,
|
||||||
|
Tooltip,
|
||||||
|
Alert,
|
||||||
|
Tag,
|
||||||
|
Divider,
|
||||||
|
DatePicker,
|
||||||
|
TimePicker,
|
||||||
|
Upload,
|
||||||
|
Progress,
|
||||||
|
Skeleton,
|
||||||
|
Popconfirm,
|
||||||
|
message,
|
||||||
|
notification
|
||||||
|
} from 'ant-design-vue'
|
||||||
|
// import VueCropper from 'vue-cropper'
|
||||||
|
|
||||||
|
Vue.use(LocaleProvider)
|
||||||
|
Vue.use(Layout)
|
||||||
|
Vue.use(Input)
|
||||||
|
Vue.use(InputNumber)
|
||||||
|
Vue.use(Button)
|
||||||
|
Vue.use(Switch)
|
||||||
|
Vue.use(Radio)
|
||||||
|
Vue.use(Checkbox)
|
||||||
|
Vue.use(Select)
|
||||||
|
Vue.use(Card)
|
||||||
|
Vue.use(Form)
|
||||||
|
Vue.use(Row)
|
||||||
|
Vue.use(Col)
|
||||||
|
Vue.use(Modal)
|
||||||
|
Vue.use(Table)
|
||||||
|
Vue.use(Tabs)
|
||||||
|
Vue.use(Icon)
|
||||||
|
Vue.use(Badge)
|
||||||
|
Vue.use(Popover)
|
||||||
|
Vue.use(Dropdown)
|
||||||
|
Vue.use(List)
|
||||||
|
Vue.use(Avatar)
|
||||||
|
Vue.use(Breadcrumb)
|
||||||
|
Vue.use(Steps)
|
||||||
|
Vue.use(Spin)
|
||||||
|
Vue.use(Menu)
|
||||||
|
Vue.use(Drawer)
|
||||||
|
Vue.use(Tooltip)
|
||||||
|
Vue.use(Alert)
|
||||||
|
Vue.use(Tag)
|
||||||
|
Vue.use(Divider)
|
||||||
|
Vue.use(DatePicker)
|
||||||
|
Vue.use(TimePicker)
|
||||||
|
Vue.use(Upload)
|
||||||
|
Vue.use(Progress)
|
||||||
|
Vue.use(Skeleton)
|
||||||
|
Vue.use(Popconfirm)
|
||||||
|
// Vue.use(VueCropper)
|
||||||
|
Vue.use(notification)
|
||||||
|
|
||||||
|
Vue.prototype.$confirm = Modal.confirm
|
||||||
|
Vue.prototype.$message = message
|
||||||
|
Vue.prototype.$notification = notification
|
||||||
|
Vue.prototype.$info = Modal.info
|
||||||
|
Vue.prototype.$success = Modal.success
|
||||||
|
Vue.prototype.$error = Modal.error
|
||||||
|
Vue.prototype.$warning = Modal.warning
|
||||||
19
ui/src/core/lazy_use.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VueStorage from 'vue-ls'
|
||||||
|
import config from '@/config/defaultSettings'
|
||||||
|
|
||||||
|
// base library
|
||||||
|
import '@/core/lazy_lib/components_use'
|
||||||
|
import Viser from 'viser-vue'
|
||||||
|
|
||||||
|
// ext library
|
||||||
|
import VueClipboard from 'vue-clipboard2'
|
||||||
|
import PermissionHelper from '@/utils/helper/permission'
|
||||||
|
|
||||||
|
VueClipboard.config.autoSetContainer = true
|
||||||
|
|
||||||
|
Vue.use(Viser)
|
||||||
|
|
||||||
|
Vue.use(VueStorage, config.storageOptions)
|
||||||
|
Vue.use(VueClipboard)
|
||||||
|
Vue.use(PermissionHelper)
|
||||||