This commit is contained in:
pycook 2019-08-28 20:51:51 +08:00 committed by pycook
parent f3046d3c91
commit 5b4f95a50e
194 changed files with 40240 additions and 16 deletions

22
.gitignore vendored
View File

@ -68,22 +68,12 @@ settings.py
*.db
# UI
node_modules
dist
# local env files
.env.local
.env.*.local
ui/node_modules
ui/dist
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
ui/npm-debug.log*
ui/yarn-debug.log*
ui/yarn-error.log*
ui/yarn.lock
# Editor directories and files
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

39
ui/.editorconfig Normal file
View File

@ -0,0 +1,39 @@
[*]
charset=utf-8
end_of_line=lf
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

3
ui/.env Normal file
View File

@ -0,0 +1,3 @@
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api

3
ui/.env.preview Normal file
View File

@ -0,0 +1,3 @@
NODE_ENV=production
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api

5
ui/.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"semi": false,
"singleQuote": true
}

7
ui/.travis.yml Normal file
View 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

21
ui/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Anan Yang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

20
ui/babel.config.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
presets: [
'@vue/app',
[
'@babel/preset-env',
{
'useBuiltIns': 'entry'
}
]
]
// if your use import on Demand, Use this code
// ,
// plugins: [
// [ 'import', {
// 'libraryName': 'ant-design-vue',
// 'libraryDirectory': 'es',
// 'style': true // `style: true` 会加载 less 文件
// } ]
// ]
}

23
ui/jest.config.js Normal file
View 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
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es6",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"],
"include": ["src/**/*"]
}

19631
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

144
ui/package.json Normal file
View File

@ -0,0 +1,144 @@
{
"name": "vue-antd-pro",
"version": "2.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"build:preview": "vue-cli-service build --mode preview",
"lint": "vue-cli-service lint",
"lint:nofix": "vue-cli-service lint --no-fix",
"test:unit": "vue-cli-service test:unit",
"postinstall": "opencollective-postinstall"
},
"dependencies": {
"@antv/data-set": "^0.10.1",
"@handsontable-pro/vue": "^3.1.1",
"@handsontable/vue": "^4.1.0",
"ant-design-vue": "^1.3.15",
"axios": "^0.19.0",
"core-js": "^2.6.5",
"enquire.js": "^2.1.6",
"handsontable": "^7.1.1",
"handsontable-pro": "^6.2.3",
"js-cookie": "^2.2.0",
"json2csv": "^4.5.2",
"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.3.3",
"vue": "^2.6.10",
"vue-clipboard2": "^0.2.1",
"vue-cropper": "0.4.4",
"vue-json-excel": "^0.2.98",
"vue-ls": "^3.2.0",
"vue-quill-editor": "^3.0.6",
"vue-router": "^3.0.1",
"vue-svg-component-runtime": "^1.0.1",
"vuedraggable": "^2.23.0",
"vuex": "^3.1.0",
"wangeditor": "^3.1.1",
"xlsx": "latest"
},
"devDependencies": {
"@ant-design/colors": "^3.1.0",
"@babel/polyfill": "^7.2.5",
"@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": "^23.6.0",
"babel-plugin-import": "^1.11.0",
"eslint": "^5.8.0",
"eslint-plugin-html": "^5.0.0",
"eslint-plugin-vue": "^5.0.0",
"less": "^3.8.1",
"less-loader": "^4.1.0",
"opencollective": "^1.0.3",
"opencollective-postinstall": "^2.0.2",
"vue-svg-icon-loader": "^2.1.1",
"vue-template-compiler": "^2.6.7",
"webpack-theme-color-replacer": "^1.2.15"
},
"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"
],
"collective": {
"type": "opencollective",
"url": "https://opencollective.com/ant-design-pro-vue"
}
}

BIN
ui/public/cmdb01.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
ui/public/cmdb02.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

7684
ui/public/color.less Normal file

File diff suppressed because it is too large Load Diff

23
ui/public/index.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<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">
<title>CMDB</title>
<style>#loading-mask{position:fixed;left:0;top:0;height:100%;width:100%;background:#fff;user-select:none;z-index:9999;overflow:hidden}.loading-wrapper{position:absolute;top:50%;left:50%;transform:translate(-50%,-100%)}.loading-dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:64px;width:64px;height:64px;box-sizing:border-box}.loading-dot i{width:22px;height:22px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.loading-dot i:nth-child(1){top:0;left:0}.loading-dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.loading-dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.loading-dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
</head>
<body>
<noscript>
<strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div id="loading-mask">
<div class="loading-wrapper">
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
</div>
</div>
<!-- built files will be auto injected -->
</body>
</html>

View 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);}}

View 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>

View 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>

View 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;}

View 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

41
ui/src/App.vue Normal file
View File

@ -0,0 +1,41 @@
<template>
<a-locale-provider :locale="locale">
<div id="app">
<router-view v-if="alive" />
</div>
</a-locale-provider>
</template>
<script>
import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import { AppDeviceEnquire } from '@/utils/mixin'
export default {
mixins: [AppDeviceEnquire],
provide () {
return {
reload: this.reload
}
},
data () {
return {
locale: zhCN,
alive: true
}
},
mounted () {},
methods: {
reload () {
this.alive = false
this.$nextTick(() => {
this.alive = true
})
}
}
}
</script>
<style>
#app {
height: 100%;
}
</style>

3
ui/src/EventBus.js Normal file
View File

@ -0,0 +1,3 @@
import Vue from 'vue'
export default new Vue()

View File

@ -0,0 +1,15 @@
import { axios } from '@/utils/request'
export function getFirstCIs (ciId) {
return axios({
url: '/v0.1/ci_relations/' + ciId + '/first_cis',
method: 'GET'
})
}
export function getSecondCIs (ciId) {
return axios({
url: '/v0.1/ci_relations/' + ciId + '/second_cis',
method: 'GET'
})
}

111
ui/src/api/cmdb/CIType.js Normal file
View File

@ -0,0 +1,111 @@
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes (parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType (CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType (data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType (CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeGroupById (CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById (CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById (groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById (groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}

View File

@ -0,0 +1,124 @@
import { axios } from '@/utils/request'
/**
* 获取 ci_type 的属性
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesByName (CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 获取 ci_type 的属性
* @param CITypeId
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesById (CITypeId, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 更新属性
* @param attrId
* @param data
* @returns {AxiosPromise}
*/
export function updateAttributeById (attrId, data) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'put',
data: data
})
}
/**
* 添加属性
* @param data
* @returns {AxiosPromise}
*/
export function createAttribute (data) {
return axios({
url: `/v0.1/attributes`,
method: 'post',
data: data
})
}
/**
* 搜索属性/ 获取所有的属性
* @param data
* @returns {AxiosPromise}
*/
export function searchAttributes (data) {
return axios({
url: `/v0.1/attributes/s`,
method: 'get',
params: data
})
}
/**
* 删除属性
* @param attrId
* @returns {AxiosPromise}
*/
export function deleteAttributesById (attrId) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'delete'
})
}
/**
* 绑定ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeAttributes (CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'post',
data: data
})
}
/**
* 更新ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeAttributesById (CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'put',
data: data
})
}
/**
* 删除ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeAttributesById (CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'delete',
data: data
})
}

View File

@ -0,0 +1,40 @@
import { axios } from '@/utils/request'
export function getCITypeChildren (CITypeID, parameter) {
return axios({
url: '/v0.1/ci_type_relations/' + CITypeID + '/children',
method: 'get',
params: parameter
})
}
export function getCITypeParent (CITypeID) {
return axios({
url: '/v0.1/ci_type_relations/' + CITypeID + '/parents',
method: 'get'
})
}
export function getRelationTypes (CITypeID, parameter) {
return axios({
url: '/v0.1/relation_types',
method: 'get',
params: parameter
})
}
export function createRelation (parentId, childrenId, relationTypeId) {
return axios({
url: `/v0.1/ci_type_relations/${parentId}/${childrenId}`,
method: 'post',
data: { relation_type_id: relationTypeId }
})
}
export function deleteRelation (parentId, childrenId) {
return axios({
url: `/v0.1/ci_type_relations/${parentId}/${childrenId}`,
method: 'delete'
})
}

67
ui/src/api/cmdb/batch.js Normal file
View File

@ -0,0 +1,67 @@
import XLSX from 'xlsx'
import { axios } from '@/utils/request'
export function processFile (fileObj) {
const promise = new Promise(function (resolve) {
const reader = new FileReader()
reader.readAsBinaryString(fileObj)
reader.onload = function (e) {
const data = e.target.result
const workbook = XLSX.read(data, { type: 'binary' })
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const lt = XLSX.utils.sheet_to_json(sheet, { header: 1 })
resolve(lt)
}
})
return promise
}
export function uploadData (ciId, data) {
data.ci_type = ciId
return axios({
url: '/v0.1/ci',
method: 'PUT',
data: data
})
}
export function writeCsv (columns) {
const { Parser } = require('json2csv')
const fields = columns
const opts = { fields }
const p = new Parser(opts)
return p.parse([])
}
export function writeExcel (columns, name) {
const worksheet = XLSX.utils.aoa_to_sheet([columns])
const newWorkBoot = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(newWorkBoot, worksheet, name)
const s = XLSX.write(newWorkBoot, { type: 'array' })
console.log(s)
return s
}
// 判断一个数组元素是否都为空的
export function any (ArrayList) {
let flag = false
for (let i = 0; i < ArrayList.length; i++) {
if (ArrayList[i]) {
flag = true
return flag
}
}
return false
}
// 去除一个二维数组 底下为空的部分
export function filterNull (twoDimArray) {
console.log(twoDimArray)
const newArray = []
for (let i = 0; i < twoDimArray.length; i++) {
if (any(twoDimArray[i])) {
newArray.push(twoDimArray[i])
}
}
return newArray
}

41
ui/src/api/cmdb/ci.js Normal file
View File

@ -0,0 +1,41 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v0.1'
export function searchCI (params) {
return axios({
url: urlPrefix + `/ci/s?${params}`,
method: 'GET'
})
}
export function addCI (params) {
return axios({
url: urlPrefix + '/ci',
method: 'POST',
data: params
})
}
export function updateCI (id, params) {
return axios({
url: urlPrefix + `/ci/${id}`,
method: 'PUT',
data: params
})
}
export function deleteCI (ciId) {
return axios({
url: urlPrefix + `/ci/${ciId}`,
method: 'DELETE'
})
}
// 获取单个ci实例
export function getCIById (ciId) {
return axios({
url: urlPrefix + `/ci/${ciId}`,
method: 'GET'
})
}

View File

@ -0,0 +1,8 @@
import { axios } from '@/utils/request'
export function getCIHistory (ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}

View File

@ -0,0 +1,41 @@
import { axios } from '@/utils/request'
export function getPreference (instance = true, tree = null) {
return axios({
url: '/v0.1/preference/ci_types',
method: 'GET',
params: { instance: instance, tree: tree }
})
}
export function getSubscribeAttributes (ciTypeId) {
return axios({
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
method: 'GET'
})
}
export function getSubscribeTreeView () {
return axios({
url: '/v0.1/preference/tree/view',
method: 'GET'
})
}
export function subscribeCIType (ciTypeId, attrs) {
return axios({
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
method: 'POST',
data: {
attr: attrs
}
})
}
export function subscribeTreeView (ciTypeId, levels) {
return axios({
url: `/v0.1/preference/tree/view`,
method: 'POST',
data: { type_id: ciTypeId, levels: levels }
})
}

14
ui/src/api/index.js Normal file
View File

@ -0,0 +1,14 @@
import config from '@/config/defaultSettings'
const api = {
Login: config.useSSO ? '/api/sso/login' : '/login',
Logout: config.useSSO ? '/api/sso/logout' : '/logout',
ForgePassword: '/auth/forge-password',
Register: '/auth/register',
twoStepCode: '/auth/2step-code',
SendSms: '/account/sms',
SendSmsErr: '/account/sms_err',
// get my info
UserInfo: '/v1/perms/user/info'
}
export default api

67
ui/src/api/login.js Normal file
View File

@ -0,0 +1,67 @@
import api from './index'
import { axios } from '@/utils/request'
import config from '@/config/defaultSettings'
/**
* login func
* parameter: {
* username: '',
* password: '',
* remember_me: true,
* captcha: '12345'
* }
* @param parameter
* @returns {*}
*/
export function login(parameter) {
return axios({
url: api.Login,
method: 'post',
data: parameter
})
}
export function getSmsCaptcha(parameter) {
return axios({
url: api.SendSms,
method: 'post',
data: parameter
})
}
export function getInfo() {
return axios({
url: api.UserInfo,
method: 'get',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
export function logout() {
console.log('logout........')
if (config.useSSO) {
window.location.replace(api.Logout)
} else {
return axios({
url: api.Logout,
method: 'post',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
}
/**
* get user 2step code open?
* @param parameter {*}
*/
export function get2step(parameter) {
return axios({
url: api.twoStepCode,
method: 'post',
data: parameter
})
}

62
ui/src/api/manage.js Normal file
View File

@ -0,0 +1,62 @@
import { axios } from '@/utils/request'
const api = {
user: '/user',
role: '/role',
service: '/service',
permission: '/permission',
permissionNoPager: '/permission/no-pager',
orgTree: '/org/tree'
}
export default api
export function getUserList (parameter) {
return axios({
url: api.user,
method: 'get',
params: parameter
})
}
export function getRoleList (parameter) {
return axios({
url: api.role,
method: 'get',
params: parameter
})
}
export function getServiceList (parameter) {
return axios({
url: api.service,
method: 'get',
params: parameter
})
}
export function getPermissions (parameter) {
return axios({
url: api.permissionNoPager,
method: 'get',
params: parameter
})
}
export function getOrgTree (parameter) {
return axios({
url: api.orgTree,
method: 'get',
params: parameter
})
}
// id == 0 add post
// id != 0 update put
export function saveService (parameter) {
return axios({
url: api.service,
method: parameter.id === 0 ? 'post' : 'put',
data: parameter
})
}

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

29
ui/src/assets/logo.svg Normal file
View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
<title>Vue</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="69.644116%" y1="0%" x2="69.644116%" y2="100%" id="linearGradient-1">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#148EFF" offset="37.8600687%"></stop>
<stop stop-color="#0A60FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="-19.8191553%" y1="-36.7931464%" x2="138.57919%" y2="157.637507%" id="linearGradient-2">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#0F78FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-3">
<stop stop-color="#FA8E7D" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="51.2635191%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
</defs>
<g id="AntVue" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(19.000000, 9.000000)">
<path d="M89.96,90.48 C78.58,93.48 68.33,83.36 67.62,82.48 L46.6604487,62.2292258 C45.5023849,61.1103236 44.8426845,59.5728835 44.8296987,57.9626396 L44.5035564,17.5209948 C44.4948861,16.4458744 44.0537714,15.4195095 43.2796864,14.6733517 L29.6459999,1.53153737 C28.055475,-0.00160504005 25.5232423,0.0449126588 23.9900999,1.63543756 C23.2715121,2.38092066 22.87,3.37600834 22.87,4.41143746 L22.87,64.3864751 C22.87,67.0807891 23.9572233,69.6611067 25.885409,71.5429748 L63.6004615,108.352061 C65.9466323,110.641873 69.6963584,110.624605 72.0213403,108.313281" id="Path-Copy" fill="url(#linearGradient-1)" fill-rule="nonzero" transform="translate(56.415000, 54.831157) scale(-1, 1) translate(-56.415000, -54.831157) "></path>
<path d="M68,90.1163122 C56.62,93.1163122 45.46,83.36 44.75,82.48 L23.7904487,62.2292258 C22.6323849,61.1103236 21.9726845,59.5728835 21.9596987,57.9626396 L21.6335564,17.5209948 C21.6248861,16.4458744 21.1837714,15.4195095 20.4096864,14.6733517 L6.7759999,1.53153737 C5.185475,-0.00160504005 2.65324232,0.0449126588 1.12009991,1.63543756 C0.401512125,2.38092066 3.90211878e-13,3.37600834 3.90798505e-13,4.41143746 L3.94351218e-13,64.3864751 C3.94681177e-13,67.0807891 1.08722326,69.6611067 3.01540903,71.5429748 L40.7807092,108.401101 C43.1069304,110.671444 46.8180151,110.676525 49.1504445,108.412561" id="Path" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
<path d="M43.2983488,19.0991931 L27.5566079,3.88246244 C26.7624281,3.11476967 26.7409561,1.84862177 27.5086488,1.05444194 C27.8854826,0.664606611 28.4044438,0.444472651 28.9466386,0.444472651 L60.3925021,0.444472651 C61.4970716,0.444472651 62.3925021,1.33990315 62.3925021,2.44447265 C62.3925021,2.9858375 62.1730396,3.50407742 61.7842512,3.88079942 L46.0801285,19.0975301 C45.3051579,19.8484488 44.0742167,19.8491847 43.2983488,19.0991931 Z" id="Path" fill="url(#linearGradient-3)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,89 @@
<template>
<div class="antd-pro-components-article-list-content-index-listContent">
<div class="description">
<slot>
{{ description }}
</slot>
</div>
<div class="extra">
<a-avatar :src="avatar" size="small" />
<a :href="href">{{ owner }}</a> 发布在 <a :href="href">{{ href }}</a>
<em>{{ updateAt | moment }}</em>
</div>
</div>
</template>
<script>
export default {
name: 'ArticleListContent',
props: {
prefixCls: {
type: String,
default: 'antd-pro-components-article-list-content-index-listContent'
},
description: {
type: String,
default: ''
},
owner: {
type: String,
required: true
},
avatar: {
type: String,
required: true
},
href: {
type: String,
required: true
},
updateAt: {
type: String,
required: true
}
}
}
</script>
<style lang="less" scoped>
@import '../index.less';
.antd-pro-components-article-list-content-index-listContent {
.description {
max-width: 720px;
line-height: 22px;
}
.extra {
margin-top: 16px;
color: @text-color-secondary;
line-height: 22px;
& /deep/ .ant-avatar {
position: relative;
top: 1px;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: top;
}
& > em {
margin-left: 16px;
color: @disabled-color;
font-style: normal;
}
}
}
@media screen and (max-width: @screen-xs) {
.antd-pro-components-article-list-content-index-listContent {
.extra {
& > em {
display: block;
margin-top: 8px;
margin-left: 0;
}
}
}
}
</style>

View File

@ -0,0 +1,3 @@
import ArticleListContent from './ArticleListContent'
export default ArticleListContent

View File

@ -0,0 +1,46 @@
<template>
<tooltip v-if="tips !== ''">
<template slot="title">{{ tips }}</template>
<avatar :size="avatarSize" :src="src" />
</tooltip>
<avatar v-else :size="avatarSize" :src="src" />
</template>
<script>
import Avatar from 'ant-design-vue/es/avatar'
import Tooltip from 'ant-design-vue/es/tooltip'
export default {
name: 'AvatarItem',
components: {
Avatar,
Tooltip
},
props: {
tips: {
type: String,
default: '',
required: false
},
src: {
type: String,
default: ''
}
},
data () {
return {
size: this.$parent.size
}
},
computed: {
avatarSize () {
return this.size !== 'mini' && this.size || 20
}
},
watch: {
'$parent.size' (val) {
this.size = val
}
}
}
</script>

View File

@ -0,0 +1,99 @@
<!--
<template>
<div :class="[prefixCls]">
<ul>
<slot></slot>
<template v-for="item in filterEmpty($slots.default).slice(0, 3)"></template>
<template v-if="maxLength > 0 && filterEmpty($slots.default).length > maxLength">
<avatar-item :size="size">
<avatar :size="size !== 'mini' && size || 20" :style="excessItemsStyle">{{ `+${maxLength}` }}</avatar>
</avatar-item>
</template>
</ul>
</div>
</template>
-->
<script>
import Avatar from 'ant-design-vue/es/avatar'
import AvatarItem from './Item'
import { filterEmpty } from '@/components/_util/util'
export default {
AvatarItem,
name: 'AvatarList',
components: {
Avatar,
AvatarItem
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-avatar-list'
},
/**
* 头像大小 类型: largesmall mini, default
* 默认值: default
*/
size: {
type: [String, Number],
default: 'default'
},
/**
* 要显示的最大项目
*/
maxLength: {
type: Number,
default: 0
},
/**
* 多余的项目风格
*/
excessItemsStyle: {
type: Object,
default: () => {
return {
color: '#f56a00',
backgroundColor: '#fde3cf'
}
}
}
},
data () {
return {}
},
methods: {
getItems (items) {
const classString = {
[`${this.prefixCls}-item`]: true,
[`${this.size}`]: true
}
if (this.maxLength > 0) {
items = items.slice(0, this.maxLength)
items.push((<Avatar size={ this.size } style={ this.excessItemsStyle }>{`+${this.maxLength}`}</Avatar>))
}
const itemList = items.map((item) => (
<li class={ classString }>{ item }</li>
))
return itemList
}
},
render () {
const { prefixCls, size } = this.$props
const classString = {
[`${prefixCls}`]: true,
[`${size}`]: true
}
const items = filterEmpty(this.$slots.default)
const itemsDom = items && items.length ? <ul class={`${prefixCls}-items`}>{ this.getItems(items) }</ul> : null
return (
<div class={ classString }>
{ itemsDom }
</div>
)
}
}
</script>

View File

@ -0,0 +1,4 @@
import AvatarList from './List'
import './index.less'
export default AvatarList

View File

@ -0,0 +1,60 @@
@import "../index";
@avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list";
@avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item";
.@{avatar-list-prefix-cls} {
display: inline-block;
ul {
list-style: none;
display: inline-block;
padding: 0;
margin: 0 0 0 8px;
font-size: 0;
}
}
.@{avatar-list-item-prefix-cls} {
display: inline-block;
font-size: @font-size-base;
margin-left: -8px;
width: @avatar-size-base;
height: @avatar-size-base;
:global {
.ant-avatar {
border: 1px solid #fff;
cursor: pointer;
}
}
&.large {
width: @avatar-size-lg;
height: @avatar-size-lg;
}
&.small {
width: @avatar-size-sm;
height: @avatar-size-sm;
}
&.mini {
width: 20px;
height: 20px;
:global {
.ant-avatar {
width: 20px;
height: 20px;
line-height: 20px;
.ant-avatar-string {
font-size: 12px;
line-height: 18px;
}
}
}
}
}

View File

@ -0,0 +1,64 @@
# AvatarList 用户头像列表
一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。
引用方式:
```javascript
import AvatarList from '@/components/AvatarList'
const AvatarListItem = AvatarList.AvatarItem
export default {
components: {
AvatarList,
AvatarListItem
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<avatar-list size="mini">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
```html
<avatar-list :max-length="3">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
## API
### AvatarList
| 参数 | 说明 | 类型 | 默认值 |
| ---------------- | -------- | ---------------------------------- | --------- |
| size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` |
| maxLength | 要显示的最大项目 | number | - |
| excessItemsStyle | 多余的项目风格 | CSSProperties | - |
### AvatarList.Item
| 参数 | 说明 | 类型 | 默认值 |
| ---- | ------ | --------- | --- |
| tips | 头像展示文案 | string | - |
| src | 头像图片连接 | string | - |

View File

@ -0,0 +1,62 @@
<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>
export default {
name: 'Bar',
props: {
title: {
type: String,
default: ''
},
data: {
type: Array,
default: () => {
return []
}
},
scale: {
type: Array,
default: () => {
return [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
}
},
tooltip: {
type: Array,
default: () => {
return [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
}
}
},
data () {
return {
}
}
}
</script>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,40 @@
<template>
<div :class="prefixCls">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="100" :data="dataSource" :scale="scale" :padding="[36, 0, 18, 0]">
<v-tooltip />
<v-smooth-line position="x*y" :size="2" />
<v-smooth-area position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
export default {
name: 'MiniSmoothArea',
props: {
prefixCls: {
type: String,
default: 'ant-pro-smooth-area'
},
scale: {
type: [Object, Array],
required: true
},
dataSource: {
type: Array,
required: true
}
},
data () {
return {
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "smooth.area.less";
</style>

View 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: '类型'
}
]
export default {
name: 'Radar',
props: {
data: {
type: Array,
default: null
}
},
data () {
return {
axis1Opts,
axis2Opts,
scale
}
}
}
</script>
<style scoped>
</style>

View 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>

View File

@ -0,0 +1,113 @@
<template>
<v-chart :width="width" :height="height" :padding="[0]" :data="data" :scale="scale">
<v-tooltip :show-title="false" />
<v-coord type="rect" direction="TL" />
<v-point position="x*y" color="category" shape="cloud" tooltip="value*category" />
</v-chart>
</template>
<script>
import { registerShape } from 'viser-vue'
const DataSet = require('@antv/data-set')
const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png'
const scale = [
{ dataKey: 'x', nice: false },
{ dataKey: 'y', nice: false }
]
registerShape('point', 'cloud', {
draw (cfg, container) {
return container.addShape('text', {
attrs: {
fillOpacity: cfg.opacity,
fontSize: cfg.origin._origin.size,
rotate: cfg.origin._origin.rotate,
text: cfg.origin._origin.text,
textAlign: 'center',
fontFamily: cfg.origin._origin.font,
fill: cfg.color,
textBaseline: 'Alphabetic',
...cfg.style,
x: cfg.x,
y: cfg.y
}
})
}
})
export default {
name: 'TagCloud',
props: {
tagList: {
type: Array,
required: true
},
height: {
type: Number,
default: 400
},
width: {
type: Number,
default: 640
}
},
data () {
return {
data: [],
scale
}
},
watch: {
tagList: function (val) {
if (val.length > 0) {
this.initTagCloud(val)
}
}
},
mounted () {
if (this.tagList.length > 0) {
this.initTagCloud(this.tagList)
}
},
methods: {
initTagCloud (dataSource) {
const { height, width } = this
const dv = new DataSet.View().source(dataSource)
const range = dv.range('value')
const min = range[0]
const max = range[1]
const imageMask = new Image()
imageMask.crossOrigin = ''
imageMask.src = imgUrl
imageMask.onload = () => {
dv.transform({
type: 'tag-cloud',
fields: ['name', 'value'],
size: [width, height],
imageMask,
font: 'Verdana',
padding: 0,
timeInterval: 5000, // max execute time
rotate () {
let random = ~~(Math.random() * 4) % 4
if (random === 2) {
random = 0
}
return random * 90 // 0, 90, 270
},
fontSize (d) {
if (d.value) {
return ((d.value - min) / (max - min)) * (32 - 8) + 8
}
return 0
}
})
this.data = dv.rows
}
}
}
}
</script>

View 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: '流量(Gb)',
alias: '流量(Gb)',
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>

View 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>

View 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;*/
}
}

View File

@ -0,0 +1,14 @@
@import "../index";
@smoothArea-prefix-cls: ~"@{ant-pro-prefix}-smooth-area";
.@{smoothArea-prefix-cls} {
position: relative;
width: 100%;
.chart-wrapper {
position: absolute;
bottom: -28px;
width: 100%;
}
}

View 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>

View File

@ -0,0 +1,3 @@
import CountDown from './CountDown'
export default CountDown

View File

@ -0,0 +1,34 @@
# CountDown 倒计时
倒计时组件。
引用方式:
```javascript
import CountDown from '@/components/CountDown/CountDown'
export default {
components: {
CountDown
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<count-down :target="new Date().getTime() + 3000000" :on-end="onEndHandle" />
```
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| target | 目标时间 | Date | - |
| onEnd | 倒计时结束回调 | funtion | -|

View File

@ -0,0 +1,153 @@
<template>
<div :class="['description-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>
.description-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>

View File

@ -0,0 +1,2 @@
import DescriptionList from './DescriptionList'
export default DescriptionList

View File

@ -0,0 +1,82 @@
<template>
<div :class="prefixCls">
<quill-editor
v-model="content"
ref="myQuillEditor"
:options="editorOption"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@ready="onEditorReady($event)"
@change="onEditorChange($event)">
</quill-editor>
</div>
</template>
<script>
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import { quillEditor } from 'vue-quill-editor'
export default {
name: 'QuillEditor',
components: {
quillEditor
},
props: {
prefixCls: {
type: String,
default: 'ant-editor-quill'
},
//
// eslint-disable-next-line
value: {
type: String
}
},
data () {
return {
content: null,
editorOption: {
// some quill options
}
}
},
methods: {
onEditorBlur (quill) {
console.log('editor blur!', quill)
},
onEditorFocus (quill) {
console.log('editor focus!', quill)
},
onEditorReady (quill) {
console.log('editor ready!', quill)
},
onEditorChange ({ quill, html, text }) {
console.log('editor change!', quill, html, text)
this.$emit('change', html)
}
},
watch: {
value (val) {
this.content = val
}
}
}
</script>
<style lang="less" scoped>
@import url('../index.less');
/* 覆盖 quill 默认边框圆角为 ant 默认圆角,用于统一 ant 组件风格 */
.ant-editor-quill {
/deep/ .ql-toolbar.ql-snow {
border-radius: @border-radius-base @border-radius-base 0 0;
}
/deep/ .ql-container.ql-snow {
border-radius: 0 0 @border-radius-base @border-radius-base;
}
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<div :class="prefixCls">
<div ref="editor" class="editor-wrapper"></div>
</div>
</template>
<script>
import WEditor from 'wangeditor'
export default {
name: 'WangEditor',
props: {
prefixCls: {
type: String,
default: 'ant-editor-wang'
},
// eslint-disable-next-line
value: {
type: String
}
},
data () {
return {
editor: null,
editorContent: null
}
},
watch: {
value (val) {
this.editorContent = val
this.editor.txt.html(val)
}
},
mounted () {
this.initEditor()
},
methods: {
initEditor () {
this.editor = new WEditor(this.$refs.editor)
// this.editor.onchangeTimeout = 200
this.editor.customConfig.onchange = (html) => {
this.editorContent = html
this.$emit('change', this.editorContent)
}
this.editor.create()
}
}
}
</script>
<style lang="less" scoped>
.ant-editor-wang {
.editor-wrapper {
text-align: left;
}
}
</style>

View 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>

View File

@ -0,0 +1,3 @@
import Ellipsis from './Ellipsis'
export default Ellipsis

View File

@ -0,0 +1,38 @@
# Ellipsis 文本自动省略号
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
引用方式:
```javascript
import Ellipsis from '@/components/Ellipsis'
export default {
components: {
Ellipsis
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<ellipsis :length="100" tooltip>
There were injuries alleged in three cases in 2015, and a
fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
</ellipsis>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
tooltip | 移动到文本展示完整内容的提示 | boolean | -
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -

View File

@ -0,0 +1,130 @@
<template>
<div class="exception">
<div class="imgBlock">
<div class="imgEle" :style="{backgroundImage: `url(${config[type].img})`}">
</div>
</div>
<div class="content">
<h1>{{ config[type].title }}</h1>
<div class="desc">{{ config[type].desc }}</div>
<div class="actions">
<a-button type="primary" @click="handleToHome">返回首页</a-button>
</div>
</div>
</div>
</template>
<script>
import types from './type'
export default {
name: 'Exception',
props: {
type: {
type: String,
default: '404'
}
},
data () {
return {
config: types
}
},
methods: {
handleToHome () {
this.$router.push({ name: 'cmdb_preference' })
}
}
}
</script>
<style lang="less">
@import "~ant-design-vue/lib/style/index";
.exception {
display: flex;
align-items: center;
height: 80%;
min-height: 500px;
.imgBlock {
flex: 0 0 62.5%;
width: 62.5%;
padding-right: 152px;
zoom: 1;
&::before,
&::after {
content: ' ';
display: table;
}
&::after {
clear: both;
height: 0;
font-size: 0;
visibility: hidden;
}
}
.imgEle {
float: right;
width: 100%;
max-width: 430px;
height: 360px;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: contain;
}
.content {
flex: auto;
h1 {
margin-bottom: 24px;
color: #434e59;
font-weight: 600;
font-size: 72px;
line-height: 72px;
}
.desc {
margin-bottom: 16px;
color: @text-color-secondary;
font-size: 20px;
line-height: 28px;
}
.actions {
button:not(:last-child) {
margin-right: 8px;
}
}
}
}
@media screen and (max-width: @screen-xl) {
.exception {
.imgBlock {
padding-right: 88px;
}
}
}
@media screen and (max-width: @screen-sm) {
.exception {
display: block;
text-align: center;
.imgBlock {
margin: 0 auto 24px;
padding-right: 0;
}
}
}
@media screen and (max-width: @screen-xs) {
.exception {
.imgBlock {
margin-bottom: -24px;
overflow: hidden;
}
}
}
</style>

View File

@ -0,0 +1,2 @@
import ExceptionPage from './ExceptionPage.vue'
export default ExceptionPage

View File

@ -0,0 +1,19 @@
const types = {
403: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
title: '403',
desc: '抱歉,你无权访问该页面'
},
404: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
title: '404',
desc: '抱歉,你访问的页面不存在或仍在开发中'
},
500: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
title: '500',
desc: '抱歉,服务器出错了'
}
}
export default types

View 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>

View File

@ -0,0 +1,4 @@
import FooterToolBar from './FooterToolBar'
import './index.less'
export default FooterToolBar

View 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;
}
}

View File

@ -0,0 +1,48 @@
# FooterToolbar 底部工具栏
固定在底部的工具栏。
## 何时使用
固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。
引用方式:
```javascript
import FooterToolBar from '@/components/FooterToolbar'
export default {
components: {
FooterToolBar
}
}
```
## 代码演示
```html
<footer-tool-bar>
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
```html
<footer-tool-bar extra="扩展信息提示">
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
children (slot) | 工具栏内容,向右对齐 | - | -
extra | 额外信息,向左对齐 | String, Object | -

View File

@ -0,0 +1,42 @@
<template>
<div class="footer">
</div>
</template>
<script>
export default {
name: 'GlobalFooter',
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, 0.45);
&:hover {
color: rgba(0, 0, 0, 0.65);
}
&:not(:last-child) {
margin-right: 40px;
}
}
}
.copyright {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
}
</style>

View File

@ -0,0 +1,2 @@
import GlobalFooter from './GlobalFooter'
export default GlobalFooter

View File

@ -0,0 +1,129 @@
<template>
<transition name="showHeader">
<div v-if="visible" class="header-animat">
<a-layout-header
v-if="visible"
:class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', ]"
: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 v-else class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggle"/>
<top-menu></top-menu>
<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" />
<a-icon v-else class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle" />
</div>
<top-menu></top-menu>
<user-menu class="header-index-right"></user-menu>
</div>
</div>
</a-layout-header>
</div>
</transition>
</template>
<script>
import UserMenu from '../tools/UserMenu'
import TopMenu from '../tools/TopMenu'
import SMenu from '../Menu/'
import Logo from '../tools/Logo'
import { mixin } from '@/utils/mixin'
export default {
name: 'GlobalHeader',
components: {
UserMenu,
TopMenu,
SMenu,
Logo
},
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 {
visible: true,
oldScrollTop: 0
}
},
mounted () {
document.addEventListener('scroll', this.handleScroll, { passive: true })
},
methods: {
handleScroll () {
if (!this.autoHideHeader) {
return
}
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop
if (!this.ticking) {
this.ticking = true
requestAnimationFrame(() => {
if (this.oldScrollTop > scrollTop) {
this.visible = true
} else if (scrollTop > 300 && this.visible) {
this.visible = false
} else if (scrollTop < 300 && !this.visible) {
this.visible = true
}
this.oldScrollTop = scrollTop
this.ticking = false
})
}
},
toggle () {
this.$emit('toggle')
}
},
beforeDestroy () {
document.body.removeEventListener('scroll', this.handleScroll, true)
}
}
</script>
<style lang="less">
@import '../index.less';
.header-animat{
position: relative;
z-index: @ant-global-header-zindex;
}
.showHeader-enter-active {
transition: all 0.25s ease;
}
.showHeader-leave-active {
transition: all 0.5s ease;
}
.showHeader-enter, .showHeader-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,2 @@
import GlobalHeader from './GlobalHeader'
export default GlobalHeader

View File

@ -0,0 +1,86 @@
<template>
<div :class="prefixCls">
<a-tabs v-model="currentTab" @change="handleTabChange">
<a-tab-pane v-for="v in icons" :tab="v.title" :key="v.key">
<ul>
<li v-for="(icon, key) in v.icons" :key="`${v.key}-${key}`" :class="{ 'active': selectedIcon==icon }" @click="handleSelectedIcon(icon)" >
<a-icon :type="icon" :style="{ fontSize: '36px' }" />
</li>
</ul>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import icons from './icons'
export default {
name: 'IconSelect',
props: {
prefixCls: {
type: String,
default: 'ant-pro-icon-selector'
},
// eslint-disable-next-line
value: {
type: String
}
},
data () {
return {
selectedIcon: this.value || '',
currentTab: 'directional',
icons
}
},
watch: {
value (val) {
this.selectedIcon = val
this.autoSwitchTab()
}
},
created () {
if (this.value) {
this.autoSwitchTab()
}
},
methods: {
handleSelectedIcon (icon) {
this.selectedIcon = icon
this.$emit('change', icon)
},
handleTabChange (activeKey) {
this.currentTab = activeKey
},
autoSwitchTab () {
icons.some(item => item.icons.some(icon => icon === this.value) && (this.currentTab = item.key))
}
}
}
</script>
<style lang="less" scoped>
@import "../index.less";
ul{
list-style: none;
padding: 0;
overflow-y: scroll;
height: 250px;
li{
display: inline-block;
padding: @padding-sm;
margin: 3px 0;
border-radius: @border-radius-base;
&:hover, &.active{
// box-shadow: 0px 0px 5px 2px @primary-color;
cursor: pointer;
color: @white;
background-color: @primary-color;
}
}
}
</style>

View File

@ -0,0 +1,48 @@
IconSelector
====
> 图标选择组件,常用于为某一个数据设定一个图标时使用
> eg: 设定菜单列表时,为每个菜单设定一个图标
该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装
### 使用方式
```vue
<template>
<div>
<icon-selector @change="handleIconChange"/>
</div>
</template>
<script>
import IconSelector from '@/components/IconSelector'
export default {
name: 'YourView',
components: {
IconSelector
},
data () {
return {
}
},
methods: {
handleIconChange (icon) {
console.log('change Icon', icon)
}
}
}
</script>
```
### 事件
| 名称 | 说明 | 类型 | 默认值 |
| ------ | -------------------------- | ------ | ------ |
| change | 当改变了 `icon` 选中项触发 | String | - |

View File

@ -0,0 +1,36 @@
/**
* 增加新的图标时请遵循以下数据结构
* Adding new icon please follow the data structure below
*/
export default [
{
key: 'directional',
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']
},
{
key: 'suggested',
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']
},
{
key: 'editor',
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']
},
{
key: 'data',
title: '数据类图标',
icons: ['area-chart', 'pie-chart', 'bar-chart', 'dot-chart', 'line-chart', 'radar-chart', 'heat-map', 'fall', 'rise', 'stock', 'box-plot', 'fund', 'sliders']
},
{
key: 'brand_logo',
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']
},
{
key: 'application',
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']
}
]

View File

@ -0,0 +1,2 @@
import IconSelector from './IconSelector'
export default IconSelector

View File

@ -0,0 +1,63 @@
<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 Logo from '@/components/tools/Logo'
import SMenu from './index'
import { mixin, mixinDevice } from '@/utils/mixin'
export default {
name: 'SideMenu',
components: { 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)
}
},
watch: {
}
}
</script>

View File

@ -0,0 +1,2 @@
import SMenu from './menu'
export default SMenu

View File

@ -0,0 +1,184 @@
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 () {
},
mounted () {
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()
const { hidden } = this.$route.meta
if (routes.length >= 3 && hidden) {
routes.pop()
this.selectedKeys = [routes[routes.length - 1].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 tag = target && 'a' || 'router-link'
const props = { to: { name: menu.name } }
const attrs = { href: menu.path, target: menu.meta.target }
if (menu.children && menu.hideChildrenInMenu) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时, selectedKeys 做控制用
menu.children.forEach(item => {
item.meta = Object.assign(item.meta, { hidden: true })
})
}
return (
<Item {...{ key: menu.path }}>
<tag {...{ props, attrs }}>
{this.renderIcon(menu.meta.icon)}
<span>{menu.meta.title}</span>
</tag>
</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>
)
}
}

View File

@ -0,0 +1,173 @@
<template>
<div style="margin: -23px -24px 24px -24px">
&lt;!&ndash;<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>&ndash;&gt;
<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>

View File

@ -0,0 +1,4 @@
import MultiTab from './MultiTab'
import './index.less'
export default MultiTab

View File

@ -0,0 +1,25 @@
@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;
}
.topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} {
max-width: 100%;
margin: 0 auto;
}

View File

@ -0,0 +1,90 @@
<template>
<a-popover
v-model="visible"
trigger="click"
placement="bottomRight"
overlayClassName="header-notice-wrapper"
:getPopupContainer="() => $refs.noticeRef.parentElement"
:autoAdjustOverflow="true"
:arrowPointAtCenter="true"
: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" ref="noticeRef">
<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>

View File

@ -0,0 +1,2 @@
import NoticeIcon from './NoticeIcon'
export default NoticeIcon

View 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>

View File

@ -0,0 +1,3 @@
import NumberInfo from './NumberInfo'
export default NumberInfo

View 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;
}
}
}
}
}

View File

@ -0,0 +1,43 @@
# NumberInfo 数据文本
常用在数据卡片中,用于突出展示某个业务数据。
引用方式:
```javascript
import NumberInfo from '@/components/NumberInfo'
export default {
components: {
NumberInfo
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<number-info
:sub-title="() => { return 'Visits this week' }"
:total="12321"
status="up"
:sub-total="17.1"></number-info>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
title | 标题 | ReactNode\|string | -
subTitle | 子标题 | ReactNode\|string | -
total | 总量 | ReactNode\|string | -
subTotal | 子总量 | ReactNode\|string | -
status | 增加状态 | 'up \| down' | -
theme | 状态样式 | string | 'light'
gap | 设置数字和描述之间的间距(像素)| number | 8

View File

@ -0,0 +1,202 @@
<template>
<div class="page-header">
<div class="page-header-index-wide">
<s-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 class="row">
<div v-if="avatar" class="avatar">
<a-avatar :src="avatar" />
</div>
<div v-if="this.$slots.content" class="headerContent">
<slot name="content"></slot>
</div>
<div v-if="this.$slots.extra" class="extra">
<slot name="extra"></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, Boolean],
default: true,
required: false
},
logo: {
type: String,
default: '',
required: false
},
avatar: {
type: String,
default: '',
required: false
}
},
data () {
return {}
}
}
</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, 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, 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>

View File

@ -0,0 +1,2 @@
import PageHeader from './PageHeader'
export default PageHeader

View File

@ -0,0 +1,10 @@
import { Spin } from 'ant-design-vue'
export default {
name: 'PageLoading',
render () {
return (<div style={{ paddingTop: 100, textAlign: 'center' }}>
<Spin size="large" />
</div>)
}
}

View File

@ -0,0 +1,109 @@
<template>
<div class="result">
<div>
<a-icon :class="{ 'icon': true, [`${type}`]: true }" :type="localIsSuccess ? 'check-circle' : 'close-circle'"/>
</div>
<div class="title">
<slot name="title">
{{ title }}
</slot>
</div>
<div class="description">
<slot name="description">
{{ description }}
</slot>
</div>
<div class="extra" v-if="$slots.default">
<slot></slot>
</div>
<div class="action" v-if="$slots.action">
<slot name="action"></slot>
</div>
</div>
</template>
<script>
const resultEnum = ['success', 'error']
export default {
name: 'Result',
props: {
/** @Deprecated */
isSuccess: {
type: Boolean,
default: false
},
type: {
type: String,
default: resultEnum[0],
validator (val) {
return (val) => resultEnum.includes(val)
}
},
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
}
},
computed: {
localIsSuccess: function () {
return this.type === resultEnum[0]
}
}
}
</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;
}
.extra {
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>

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