Merge branch 'master' of git.komect.net:ISG/secogateway

This commit is contained in:
黄昕 2019-07-03 16:07:47 +08:00
commit f056710c6b
188 changed files with 44640 additions and 789 deletions

1
.gitignore vendored
View File

@ -21,3 +21,4 @@ Product/build/debug/
.build/
_install/
.idea/
/ControlPlatform/web/node_modules

View File

@ -84,6 +84,7 @@ enum commcfgmsgtype{
//COMMMSGNL_BASE = NLMSG_MIN_TYPE,/*netlink 保留控制消息*/
COMMMSGNL_BASE = 0x10,/*netlink 保留控制消息*/
COMMNMSG_CFG_DEBUGFS = 0x11,/*keep the same with NLMSG_PDELIV_DEBUGFS */
FREEAUTH_CFG = 0x13, /*用户态发送给内核态的免认证规则消息*/
COMMNMSG_POLICYCONF,
NK_DEBUGFS_PRK_ONOFF_CFG = 0X16,/*keep the same with DEBUGFS PRINTK ON OR OFF */

View File

@ -1,11 +1,11 @@
/* This file is auto generated,for sGATE version info */
/* Used readelf to get this information form driver of application */
/* "readelf --debug-dump=macro <filename>" */
#define sGATE_COMPILE_DATE "2019-06-19"
#define sGATE_COMPILE_TIME "14:18:13"
#define sGATE_COMPILE_MAJOR "20190619"
#define sGATE_COMPILE_SUB "141813"
#define sGATE_COMPILE_BY "hx"
#define sGATE_COMPILE_DATE "2019-07-01"
#define sGATE_COMPILE_TIME "17:53:10"
#define sGATE_COMPILE_MAJOR "20190701"
#define sGATE_COMPILE_SUB "175310"
#define sGATE_COMPILE_BY "cl"
#define sGATE_COMPILE_HOST "esgwdev01"
#define sGATE_GIT_TAGS "c0ad51e6f-dev"
#define sGATE_GIT_VERS "c0ad51e6f27589e51268ec92a14ee1cb701a2d5f"
#define sGATE_GIT_TAGS "aaa812c65-dev"
#define sGATE_GIT_VERS "aaa812c654225f595f12a32bc7d56bcc225f3ee4"

View File

@ -81,8 +81,30 @@ extern "C" {
S2J_STRUCT_GET_STRUCT_ELEMENT(child_struct, to_struct, child_json, from_json, type, element)
/* s2j.c */
extern S2jHook s2jHook;
void s2j_init(S2jHook *hook);
//extern S2jHook s2jHook;
S2jHook s2jHook = {
.malloc_fn = malloc,
.free_fn = free,
};
static void s2j_init(S2jHook *hook)
{
/* initialize cJSON library */
if(hook == NULL)
{
hook = &s2jHook;
}
cJSON_InitHooks((cJSON_Hooks *)hook);
/* initialize hooks */
if (hook) {
s2jHook.malloc_fn = (hook->malloc_fn) ? hook->malloc_fn : malloc;
s2jHook.free_fn = (hook->free_fn) ? hook->free_fn : free;
} else {
s2jHook.malloc_fn = malloc;
s2jHook.free_fn = free;
}
}
#ifdef __cplusplus
}

View File

@ -0,0 +1,18 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"],
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
}
}
}

View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -0,0 +1,5 @@
/build/
/config/
/dist/
/*.js
/test/unit/coverage/

View File

@ -0,0 +1,29 @@
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
env: {
browser: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}

5
ControlPlatform/web/.gitattributes vendored Normal file
View File

@ -0,0 +1,5 @@
*.js linguist-language=vue
*.css linguist-language=vue
*.html linguist-language=vue

View File

@ -0,0 +1,10 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}

View File

@ -0,0 +1,41 @@
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})

View File

@ -0,0 +1,54 @@
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,101 @@
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}

View File

@ -0,0 +1,22 @@
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}

View File

@ -0,0 +1,110 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
const webpack = require('webpack');
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: ['babel-polyfill', './src/main.js']
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'~': resolve('src/components'),
'utils': resolve('src/utils')
}
},
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
},
plugins: [
// Ignore all locale files of moment.js
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new webpack.optimize.UglifyJsPlugin({
beautify: false,
comments: false,
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true,
}
}),
]
}

View File

@ -0,0 +1,95 @@
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})

View File

@ -0,0 +1,147 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = process.env.NODE_ENV === 'testing'
? require('../config/test.env')
: require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing'
? 'index.html'
: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(js|css)$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

View File

@ -0,0 +1,7 @@
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

View File

@ -0,0 +1,84 @@
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'api': {
target: 'http://192.168.100.98:8181',
changeorigin: true,
pathrewrite: {
'^/api': '/'
}
}
},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8081, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: './',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: true,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: true
}
}

View File

@ -0,0 +1,4 @@
'use strict'
module.exports = {
NODE_ENV: '"production"'
}

View File

@ -0,0 +1,7 @@
'use strict'
const merge = require('webpack-merge')
const devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="shortcut icon" href="static/img/favicon.ico" type="image/icon">
<title>SDN 控制平台</title>
<style>.project-loading{background:#fff;width:100%;height:100%;position:fixed;left:0;top:0;z-index:100000}.project-loading .loader{position:fixed;top:50%;left:50%}@-webkit-keyframes line-scale-pulse-out{0%{-webkit-transform:scaley(1);transform:scaley(1)}50%{-webkit-transform:scaley(.4);transform:scaley(.4)}100%{-webkit-transform:scaley(1);transform:scaley(1)}}@keyframes line-scale-pulse-out{0%{-webkit-transform:scaley(1);transform:scaley(1)}50%{-webkit-transform:scaley(.4);transform:scaley(.4)}100%{-webkit-transform:scaley(1);transform:scaley(1)}}.line-scale-pulse-out>div{width:3px;height:33px;border-radius:2px;margin:1px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block;-webkit-animation:line-scale-pulse-out .9s -.6s infinite cubic-bezier(.85,.25,.37,.85);animation:line-scale-pulse-out .9s -.6s infinite cubic-bezier(.85,.25,.37,.85)}.line-scale-pulse-out>div:nth-child(2),.line-scale-pulse-out>div:nth-child(4){-webkit-animation-delay:-.4s!important;animation-delay:-.4s!important}.line-scale-pulse-out>div:nth-child(1),.line-scale-pulse-out>div:nth-child(5){-webkit-animation-delay:-.2s!important;animation-delay:-.2s!important}</style>
</head>
<body>
<div id="cmcc">
<div class="project-loading">
<div class="loader">
<div class="loader-inner line-scale-pulse-out">
<div style="background:#f95476"></div>
<div style="background:#ffb74e"></div>
<div style="background:#4886ff"></div>
<div style="background:#ffb74e"></div>
<div style="background:#f95476"></div>
</div>
</div>
</div>
</div>
</body>
</html>

14026
ControlPlatform/web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,95 @@
{
"name": "cmcc-front",
"version": "1.0.0",
"description": "CMCC Front By Vue",
"author": "CMHI <http://hy.10086.cn/>",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"build": "node build/build.js"
},
"dependencies": {
"ant-design-vue": "^1.3.7",
"apexcharts": "^2.6.0",
"axios": "^0.18.0",
"babel-polyfill": "^6.26.0",
"date-fns": "^1.29.0",
"enquire.js": "^2.1.6",
"vue": "^2.5.17",
"vue-apexcharts": "^1.2.7",
"vue-router": "^3.0.1",
"vuedraggable": "^2.16.0",
"vuex": "^3.0.1"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-jest": "^21.0.2",
"babel-loader": "^7.1.1",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^2.0.1",
"chromedriver": "^2.27.2",
"compression-webpack-plugin": "^1.1.12",
"copy-webpack-plugin": "^4.0.1",
"cross-spawn": "^5.0.1",
"css-loader": "^0.28.0",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"jest": "^22.0.4",
"jest-serializer-vue": "^0.3.0",
"less": "^3.7.1",
"less-loader": "^4.1.0",
"nightwatch": "^0.9.12",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"selenium-server": "^3.0.1",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-jest": "^1.0.2",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "2.5.17",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

View File

@ -0,0 +1,191 @@
<template>
<a-locale-provider :locale="chinese">
<div id="cmcc">
<router-view/>
</div>
</a-locale-provider>
</template>
<script>
import enquireScreen from './utils/device'
import chinese from 'ant-design-vue/lib/locale-provider/zh_CN'
import 'moment/locale/zh-cn'
export default {
name: 'cmcc',
data () {
return {
chinese
}
},
created () {
let _this = this
enquireScreen(isMobile => {
_this.$store.commit('setting/setDevice', isMobile)
})
}
}
</script>
<style lang="less">
:global {
.dragable-ghost {
border: 1px dashed #aaaaaa;
opacity: 0.65;
}
.dragable-chose {
border: 1px dashed #aaaaaa;
opacity: 0.65;
}
.dragable-drag {
border: 1px dashed #aaaaaa;
opacity: 0.65;
}
}
::-webkit-scrollbar {
width: .5rem;
height: .5rem;
}
::-webkit-scrollbar-track {
border-radius: 1px;
}
::-webkit-scrollbar-thumb {
border-radius: 1px;
background: rgba(0, 0, 0, .2);
}
.multi-page {
margin: -24px 0 0
}
.single-page {
margin: 0;
}
.card-area {
width: 100%
}
.not-menu-page {
background: #fff;
padding: 16px 32px;
border-left: 1px solid #e8e8e8;
border-right: 1px solid #e8e8e8;
border-bottom: 1px solid #e8e8e8;
}
.ant-btn, .ant-input, .input, .ant-pagination-item, .ant-pagination-prev .ant-pagination-item-link,
.ant-pagination-next .ant-pagination-item-link, .ant-tag, .ant-modal-content, .ant-select-selection,
.ant-select-dropdown, .ant-input-group-addon, .ant-input-number-input, .ant-input-number,
.ant-pagination-options-quick-jumper input, .ant-alert {
border-radius: 2px !important;
}
.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab, .ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab {
border-radius: 3px 3px 0 0 !important;
}
.ant-card-wider-padding .ant-card-body {
padding: 5px 10px !important;
}
.ant-modal-mask {
background-color: rgba(0, 0, 0, 0.6) !important;
}
.ant-modal-header {
border-bottom-color: #fff !important;
}
.ant-menu-dark .ant-menu-inline.ant-menu-sub {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) inset !important;
}
.ant-menu-inline .ant-menu-item, .ant-menu-vertical .ant-menu-item {
margin-top: 0 !important;
}
.ant-menu-dark, .ant-menu-dark .ant-menu-sub {
background: #393e46 !important;
}
.ant-table-row-expand-icon {
margin-right: 1rem !important;
}
:root .ant-tabs-tab-prev {
border: 1px solid #e8e8e8;
border-radius: 3px 0 0 0;
background: #fafafa;
}
.ant-card-loading{
&:after{
width: 0 !important;
}
}
.ant-tabs-tab-next.ant-tabs-tab-arrow-show {
border: 1px solid #e8e8e8;
border-radius: 0 3px 0 0;
background: #fafafa;
}
.ant-layout-header, .system-top-menu {
height: 59px !important;
line-height: 59px !important;
}
.ant-form-item {
margin-bottom: 1rem !important;
}
.ant-menu-inline, .ant-menu-vertical, .ant-menu-vertical-left {
border-right: 0 solid #ccc !important;
}
.ant-drawer-body {
padding-bottom: 3rem !important;
}
.page-tabs .ant-tabs-close-x {
color:#fff !important;
margin-left: 0.3rem! important;
}
.page-tabs:hover .ant-tabs-close-x {
color: #f95476 !important;
}
.drawer-bootom-button {
position: absolute;
bottom: 0;
width: 100%;
border-top: 1px solid #e8e8e8;
padding: 10px 16px;
text-align: right;
left: 0;
background: #fff;
border-radius: 0 0 2px 2px;
}
.search {
margin-bottom: .5rem !important;
}
i {
font-size: .97rem;
}
p {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
@media screen and (min-width: 1400px) {
}
@media screen and (max-width: 1399px) {
p, .ant-pagination, .ant-form, .ant-dropdown, .ant-form-item, .ant-select, .ant-breadcrumb, .ant-form label, .ant-btn, .ant-table,
.ant-menu-vertical .ant-menu-item, .ant-menu-vertical-left .ant-menu-item, .ant-menu-vertical-right .ant-menu-item, .ant-menu-inline .ant-menu-item, .ant-menu-vertical .ant-menu-submenu-title, .ant-menu-vertical-left .ant-menu-submenu-title, .ant-menu-vertical-right .ant-menu-submenu-title, .ant-menu-inline .ant-menu-submenu-title {
font-size: 13px !important;
}
.ant-card-head {
font-size: 14px !important;
}
.page-tabs .ant-tabs-nav-container {
font-size: 13px !important;
}
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div class="theme-color" :style="{backgroundColor: color}" @click="toggle">
<a-icon v-if="sChecked" type="check" />
</div>
</template>
<script>
const Group = {
name: 'ColorCheckboxGroup',
props: {
defaultValues: {
required: false
},
multiple: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
values: [],
options: []
}
},
computed: {
colors () {
let colors = []
this.options.forEach(item => {
if (item.sChecked) {
colors.push(item.color)
}
})
return colors
}
},
provide () {
return {
groupContext: this
}
},
watch: {
values: function (newVal, oldVal) {
if (!(newVal.length === 1 && oldVal.length === 1 && newVal[0] === oldVal[0]) || this.multiple) {
this.$emit('change', this.values, this.colors)
}
}
},
methods: {
handleChange (option) {
if (!option.checked) {
this.values = this.values.filter(item => item !== option.value)
} else {
if (!this.multiple) {
this.values = [option.value]
this.options.forEach(item => {
if (item.value !== option.value) {
item.sChecked = false
}
})
} else {
this.values.push(option.value)
}
}
}
},
render (h) {
const clear = h('div', {attrs: {style: 'clear: both'}})
return h(
'div',
{},
[this.$slots.default, clear]
)
}
}
export default {
name: 'ColorCheckbox',
Group: Group,
props: {
color: {
required: true
},
value: {
required: true
},
checked: {
required: false,
default: false
}
},
data () {
return {
sChecked: this.checked
}
},
inject: ['groupContext'],
watch: {
'sChecked': function (val) {
const value = {
value: this.value,
color: this.color,
checked: this.sChecked
}
this.$emit('change', value)
const groupContext = this.groupContext
if (groupContext) {
groupContext.handleChange(value)
}
}
},
created () {
const groupContext = this.groupContext
if (groupContext) {
this.sChecked = groupContext.defaultValues.indexOf(this.value) >= 0
groupContext.options.push(this)
}
},
methods: {
toggle () {
this.sChecked = !this.sChecked
}
}
}
</script>
<style lang="less" scoped>
.theme-color{
float: left;
width: 20px;
height: 20px;
border-radius: 2px;
cursor: pointer;
margin-right: 8px;
text-align: center;
color: #fff;
font-weight: bold;
margin-top: .5rem;
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<div class="img-check-box" @click="toggle">
<img :src="img" />
<div v-if="sChecked" class="check-item">
<a-icon type="check" />
</div>
</div>
</template>
<script>
const Group = {
name: 'ImgCheckboxGroup',
props: {
multiple: {
type: Boolean,
required: false,
default: false
},
defaultValues: {
type: Array,
required: false,
default: () => []
}
},
data () {
return {
values: [],
options: []
}
},
provide () {
return {
groupContext: this
}
},
watch: {
'values': function (newVal, oldVal) {
// chang
if (!(newVal.length === 1 && oldVal.length === 1 && newVal[0] === oldVal[0])) {
this.$emit('change', this.values)
}
}
},
methods: {
handleChange (option) {
if (!option.checked) {
this.values = this.values.filter(item => item !== option.value)
} else {
if (!this.multiple) {
this.values = [option.value]
this.options.forEach(item => {
if (item.value !== option.value) {
item.sChecked = false
}
})
} else {
this.values.push(option.value)
}
}
}
},
render (h) {
return h(
'div',
{
attrs: {style: 'display: flex'}
},
[this.$slots.default]
)
}
}
export default {
name: 'ImgCheckbox',
Group,
props: {
checked: {
type: Boolean,
required: false,
default: false
},
img: {
type: String,
required: true
},
value: {
required: true
}
},
data () {
return {
sChecked: this.checked
}
},
inject: ['groupContext'],
watch: {
'sChecked': function (val) {
const option = {
value: this.value,
checked: this.sChecked
}
this.$emit('change', option)
const groupContext = this.groupContext
if (groupContext) {
groupContext.handleChange(option)
}
}
},
created () {
const groupContext = this.groupContext
if (groupContext) {
this.sChecked = groupContext.defaultValues.length > 0 ? groupContext.defaultValues.indexOf(this.value) >= 0 : this.sChecked
groupContext.options.push(this)
}
},
methods: {
toggle () {
if (this.sChecked) {
return
}
this.sChecked = !this.sChecked
}
}
}
</script>
<style lang="less" scoped>
.img-check-box{
margin-right: 16px;
position: relative;
border-radius: 4px;
cursor: pointer;
.check-item{
position: absolute;
top: 0;
right: 0;
width: 100%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: #1890ff;
font-size: 14px;
font-weight: bold;
}
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<a-range-picker
:key="id"
ref="rangeDate"
@change="onChange" style="width: 100%"></a-range-picker>
</template>
<script>
export default {
name: 'RangeDate',
data () {
return {
id: +new Date()
}
},
methods: {
onChange (date, dateString) {
this.$emit('change', dateString)
},
reset () {
this.id = +new Date()
}
}
}
</script>

View File

@ -0,0 +1,71 @@
<template>
<div class="exception">
<div class="img">
<img :src="config[type].img" />
<!--<div class="ele" :style="{backgroundImage: `url(${config[type].img})`}"/>-->
</div>
<div class="content">
<h1>{{config[type].title}}</h1>
<div class="desc">{{config[type].desc}}</div>
<div class="action">
<a-button type="primary" @click="returnHome">带我回首页</a-button>
</div>
</div>
</div>
</template>
<script>
import Config from './typeConfig'
export default {
name: 'ExceptionPage',
props: ['type'],
data () {
return {
config: Config
}
},
methods: {
returnHome () {
this.$router.push('/')
}
}
}
</script>
<style lang="less" scoped>
.exception{
min-height: 500px;
height: 80%;
align-items: center;
text-align: center;
margin-top: 150px;
.img{
display: inline-block;
padding-right: 52px;
zoom: 1;
img{
height: 360px;
max-width: 430px;
}
}
.content{
display: inline-block;
flex: auto;
h1{
color: #434e59;
font-size: 72px;
font-weight: 600;
line-height: 72px;
margin-bottom: 24px;
}
.desc{
color: rgba(0,0,0,.45);
font-size: 20px;
line-height: 28px;
margin-bottom: 16px;
}
}
}
</style>

View File

@ -0,0 +1,19 @@
const config = {
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 config

View File

@ -0,0 +1,71 @@
<template>
<a-menu :style="style" class="contextmenu" v-show="visible" @click="handleClick" :selectedKeys="selectedKeys">
<a-menu-item :key="item.key" v-for="item in itemList">
<a-icon role="menuitemicon" v-if="item.icon" :type="item.icon" />{{item.text}}
</a-menu-item>
</a-menu>
</template>
<script>
export default {
name: 'Contextmenu',
props: {
visible: {
type: Boolean,
required: false,
default: false
},
itemList: {
type: Array,
required: true,
default: () => []
}
},
data () {
return {
left: 0,
top: 0,
target: null,
selectedKeys: []
}
},
computed: {
style () {
return {
left: this.left + 'px',
top: this.top + 'px'
}
}
},
created () {
window.addEventListener('mousedown', e => this.closeMenu(e))
window.addEventListener('contextmenu', e => this.setPosition(e))
},
methods: {
closeMenu (e) {
if (['menuitemicon', 'menuitem'].indexOf(e.target.getAttribute('role')) < 0) {
this.$emit('update:visible', false)
}
},
setPosition (e) {
this.left = e.clientX
this.top = e.clientY
this.target = e.target
},
handleClick ({key}) {
this.$emit('select', key, this.target)
this.$emit('update:visible', false)
}
}
}
</script>
<style lang="less" scoped>
.contextmenu{
position: fixed;
z-index: 2;
border: 1px solid #eee;
border-radius: 4px;
box-shadow: 2px 2px 5px #e8e8e8 !important;
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<a-layout-sider
:class="[theme, 'sider', isMobile ? null : 'shadow', fixSiderbar? 'ant-fixed-sidemenu' : null]"
width="256px"
:collapsible="collapsible"
v-model="collapsed"
:trigger="null">
<div :class="['logo', theme]">
<router-link to="/">
<img src="static/img/logo.png" alt="">
<h1 class="animated fadeIn">{{systemName}}</h1>
</router-link>
</div>
<i-menu :theme="theme" :collapsed="collapsed" :menuData="menuData" @select="onSelect"/>
</a-layout-sider>
</template>
<script>
import IMenu from './menu'
import { mapState } from 'vuex'
export default {
name: 'SiderMenu',
components: {IMenu},
props: {
collapsible: {
type: Boolean,
required: false,
default: false
},
collapsed: {
type: Boolean,
required: false,
default: false
},
menuData: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
}
},
computed: {
...mapState({
isMobile: state => state.setting.isMobile,
systemName: state => state.setting.systemName,
fixSiderbar: state => state.setting.fixSiderbar
})
},
methods: {
onSelect (obj) {
this.$emit('menuSelect', obj)
}
}
}
</script>
<style lang="less" scoped>
.shadow {
box-shadow: 1px 0 6px rgba(0, 21, 41, .35);
}
.sider {
z-index: 16;
position: relative;
overflow-x: hidden;
&.light {
background-color: #fff;
}
&.dark {
background-color: #393e46;
}
&.ant-fixed-sidemenu {
position: fixed;
overflow-y: auto;
height: 100%;
}
.logo {
height: 59px;
position: relative;
line-height: 59px;
padding-left: 24px;
-webkit-transition: all .3s;
transition: all .3s;
overflow: hidden;
&.light {
background-color: #fff;
border-bottom: 1px solid #f8f8f8;
}
&.dark {
background-color: #393e46;
h1 {
color: #fff;
}
}
h1 {
color: #fff;
font-size: 20px;
margin: 0 0 0 12px;
font-family: Chinese Quote,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
font-weight: 600;
display: inline-block;
height: 32px;
line-height: 32px;
vertical-align: middle;
}
img {
width: 32px;
display: inline-block;
vertical-align: middle;
}
}
}
</style>

View File

@ -0,0 +1,170 @@
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
const {Item, SubMenu} = Menu
export default {
name: 'IMenu',
props: {
menuData: {
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) => {
let keys = []
vm.menuData.forEach(item => {
keys.push(item.path)
})
return keys
}
},
created () {
this.updateMenu()
},
watch: {
collapsed (val) {
if (val) {
this.cachedOpenKeys = this.openKeys
this.openKeys = []
} else {
this.openKeys = this.cachedOpenKeys
}
},
'$route': function () {
this.updateMenu()
}
},
methods: {
renderIcon: function (h, icon) {
return icon === 'none' ? null
: h(
Icon,
{
props: {type: icon}
})
},
renderMenuItem: function (h, menu, pIndex, index) {
return h(
Item,
{
key: menu.path ? menu.path : 'item_' + pIndex + '_' + index
},
[
h(
'a',
{attrs: {href: '#' + menu.path}},
[
this.renderIcon(h, menu.icon),
h('span', [menu.name])
]
)
]
)
},
renderSubMenu: function (h, menu, pIndex, index) {
const this2_ = this
let subItem = [h('span',
{slot: 'title'},
[
this.renderIcon(h, menu.icon),
h('span', [menu.name])
]
)]
let itemArr = []
let pIndex_ = pIndex + '_' + index
menu.children.forEach(function (item, i) {
itemArr.push(this2_.renderItem(h, item, pIndex_, i))
})
return h(
SubMenu,
{key: menu.path ? menu.path : 'submenu_' + pIndex + '_' + index},
subItem.concat(itemArr)
)
},
renderItem: function (h, menu, pIndex, index) {
if (!menu.hidden) {
return menu.children ? this.renderSubMenu(h, menu, pIndex, index) : this.renderMenuItem(h, menu, pIndex, index)
}
},
renderMenu: function (h, menuTree) {
const this2_ = this
let menuArr = []
menuTree.forEach(function (menu, i) {
if (!menu.hidden) {
menuArr.push(this2_.renderItem(h, menu, '0', i))
}
})
return menuArr
},
onOpenChange (openKeys) {
const latestOpenKey = openKeys.find(key => this.openKeys.indexOf(key) === -1)
if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
this.openKeys = openKeys
} else {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
},
updateMenu () {
let routes = this.$route.matched.concat()
if (routes.length >= 4 && this.$route.meta.hidden) {
routes.pop()
this.selectedKeys = [routes[2].path]
} else {
this.selectedKeys = [routes.pop().path]
}
let openKeys = []
if (this.mode === 'inline') {
routes.forEach((item) => {
openKeys.push(item.path)
})
}
this.collapsed ? this.cachedOpenKeys = openKeys : this.openKeys = openKeys
}
},
render (h) {
return h(
Menu,
{
props: {
theme: this.$props.theme,
mode: this.$props.mode,
openKeys: this.openKeys,
selectedKeys: this.selectedKeys
},
on: {
openChange: this.onOpenChange,
select: (obj) => {
this.selectedKeys = obj.selectedKeys
this.$emit('select', obj)
}
}
}, this.renderMenu(h, this.menuData)
)
}
}

View File

@ -0,0 +1,139 @@
<template>
<a-layout-sider class="sider" width="273">
<setting-item title="导航栏颜色">
<img-checkbox-group @change="setTheme">
<img-checkbox img="static/img/side-bar-dark.svg" :checked="dark"
value="dark"/>
<img-checkbox img="static/img/side-bar-light.svg" :checked="!dark"
value="light"/>
</img-checkbox-group>
</setting-item>
<setting-item title="主题颜色">
<color-checkbox-group @change="onColorChange" :defaultValues="defaultValues" :multiple="false">
<template v-for="(color, index) in colorList">
<color-checkbox :color="color" :value="index + 1" :key="index"/>
</template>
</color-checkbox-group>
</setting-item>
<a-divider/>
<setting-item title="导航栏位置">
<img-checkbox-group @change="setLayout">
<img-checkbox img="static/img/side-bar-left.svg" :checked="side" value="side"/>
<img-checkbox img="static/img/side-bar-top.svg" :checked="!side" value="head"/>
</img-checkbox-group>
</setting-item>
<setting-item>
<a-list :split="false">
<a-list-item>
固定顶栏
<a-switch :checked="fixedHeader" slot="actions" size="small" @change="fixHeader"/>
</a-list-item>
<a-list-item>
固定侧边栏
<a-switch :checked="fixedSiderbar" slot="actions" size="small" @change="fixSiderbar"/>
</a-list-item>
<a-list-item>
多页签模式
<a-switch :checked="multipage" slot="actions" size="small" @change="setMultipage"/>
</a-list-item>
</a-list>
</setting-item>
<a-button style="width: 100%" icon="save" @click="updateUserConfig">保存设置</a-button>
</a-layout-sider>
</template>
<script>
import SettingItem from './SettingItem'
import StyleItem from './StyleItem'
import ColorCheckbox from '../checkbox/ColorCheckbox'
import ImgCheckbox from '../checkbox/ImgCheckbox'
import { updateTheme } from 'utils/color'
import {mapState, mapMutations} from 'vuex'
const ColorCheckboxGroup = ColorCheckbox.Group
const ImgCheckboxGroup = ImgCheckbox.Group
export default {
name: 'Setting',
components: {ImgCheckboxGroup, ImgCheckbox, ColorCheckboxGroup, ColorCheckbox, StyleItem, SettingItem},
computed: {
...mapState({
multipage: state => state.setting.multipage,
theme: state => state.setting.theme,
colorList: state => state.setting.colorList,
fixedSiderbar: state => state.setting.fixSiderbar,
fixedHeader: state => state.setting.fixHeader,
layout: state => state.setting.layout,
color: state => state.setting.color,
user: state => state.account.user
}),
dark () {
return this.theme === 'dark'
},
side () {
return this.layout === 'side'
},
defaultValues () {
let currentColor = this.$store.state.setting.color
if (Array.isArray(currentColor)) {
currentColor = currentColor[0]
}
let index = this.colorList.indexOf(currentColor) + 1
return `[${index}]`
}
},
methods: {
...mapMutations({setSettingBar: 'setting/setSettingBar'}),
onColorChange (values, colors) {
if (colors.length > 0) {
updateTheme(colors)
this.$store.commit('setting/setColor', colors)
}
},
setTheme (values) {
this.$store.commit('setting/setTheme', values[0])
},
setLayout (values) {
this.$store.commit('setting/setLayout', values[0])
},
setMultipage (checked) {
this.$store.commit('setting/setMultipage', checked)
},
fixSiderbar (checked) {
this.$store.commit('setting/fixSiderbar', checked)
},
fixHeader (checked) {
this.$store.commit('setting/fixHeader', checked)
},
updateUserConfig () {
this.$put('user/userconfig', {
multiPage: this.multipage ? '1' : '0',
theme: this.theme,
fixedSiderbar: this.fixedSiderbar ? '1' : '0',
fixedHeader: this.fixedHeader ? '1' : '0',
layout: this.layout,
color: this.color,
userId: this.user.userId
}).then(() => {
this.$message.success('保存成功')
this.setSettingBar(false)
})
}
}
}
</script>
<style lang="less" scoped>
.sider {
background-color: #fff;
height: 100%;
padding: 24px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
position: relative;
.flex {
display: flex;
}
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="setting-item">
<h3 class="title">{{title}}</h3>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'SettingItem',
props: ['title']
}
</script>
<style lang="less" scoped>
.setting-item{
margin-bottom: 24px;
.title{
font-size: 14px;
color: rgba(0,0,0,.85);
line-height: 22px;
margin-bottom: 12px;
}
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div class="style">
<img :src="img" />
<div v-if="selected" class="select-item">
<a-icon type="check" />
</div>
</div>
</template>
<script>
export default {
name: 'StyleItem',
props: ['selected', 'img']
}
</script>
<style lang="less" scoped>
.style{
margin-right: 16px;
position: relative;
border-radius: 4px;
cursor: pointer;
.select-item{
position: absolute;
top: 0;
right: 0;
width: 100%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: #1890ff;
font-size: 14px;
font-weight: bold;
}
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<div >
<div :class="['mask', openDrawer ? 'open' : 'close']" @click="close"></div>
<div :class="['drawer', placement, openDrawer ? 'open' : 'close']">
<div ref="drawer" style="position: relative; height: 100%;">
<slot></slot>
</div>
<div v-if="showHandler" :class="['handler-container', placement]" ref="handler">
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
name: 'Drawer',
data () {
return {
drawerWidth: 0
}
},
props: {
openDrawer: {
type: Boolean,
required: false,
default: false
},
placement: {
type: String,
required: false,
default: 'left'
},
showHandler: {
type: Boolean,
required: false,
default: true
}
},
mounted () {
this.drawerWidth = this.getDrawerWidth()
},
watch: {
'drawerWidth': function (val) {
if (this.placement === 'left') {
this.$refs.handler.style.left = val + 'px'
} else {
this.$refs.handler.style.right = val + 'px'
}
}
},
methods: {
close () {
this.setSettingBar(false)
},
getDrawerWidth () {
return this.$refs.drawer.clientWidth
},
...mapMutations({setSettingBar: 'setting/setSettingBar'})
}
}
</script>
<style lang="less" scoped>
.mask{
position: fixed;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.2);
transition: all 0.5s;
z-index: 100;
&.open{
display: inline-block;
}
&.close{
display: none;
}
}
.drawer{
position: fixed;
height: 100%;
transition: all 0.5s;
z-index: 100;
&.left{
left: 0px;
&.open{
box-shadow: 2px 0 8px rgba(0,0,0,.15);
}
&.close{
transform: translateX(-100%);
}
}
&.right{
right: 0px;
&.open{
box-shadow: -2px 0 8px rgba(0,0,0,.15);
}
&.close{
transform: translateX(100%);
}
}
.sider{
height: 100%;
}
}
.handler-container{
position: fixed;
top: 200px;
text-align: center;
transition: all 0.5s;
cursor: pointer;
.handler {
height: 40px;
width: 40px;
background-color: #fff;
z-index: 100;
font-size: 26px;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
line-height: 40px;
}
&.left{
.handler{
border-radius: 0 5px 5px 0;
}
}
&.right{
.handler{
border-radius: 5px 0 0 5px;
}
}
}
</style>

View File

@ -0,0 +1,40 @@
import Vue from 'vue'
import CMCC from './CMCC'
import router from './router'
import Antd from 'ant-design-vue'
import store from './store'
import request from 'utils/request'
import db from 'utils/localstorage'
import VueApexCharts from 'vue-apexcharts'
import 'ant-design-vue/dist/antd.css'
import 'utils/install'
Vue.config.productionTip = false
Vue.use(Antd)
Vue.use(db)
Vue.use(VueApexCharts)
Vue.component('apexchart', VueApexCharts)
Vue.use({
install (Vue) {
Vue.prototype.$db = db
}
})
Vue.prototype.$post = request.post
Vue.prototype.$get = request.get
Vue.prototype.$put = request.put
Vue.prototype.$delete = request.delete
Vue.prototype.$export = request.export
Vue.prototype.$download = request.download
Vue.prototype.$upload = request.upload
/* eslint-disable no-new */
new Vue({
router,
store,
render: h => h(CMCC)
}).$mount('#cmcc')

View File

@ -0,0 +1,112 @@
import Vue from 'vue'
import Router from 'vue-router'
import MenuView from '@/views/common/MenuView'
import PageView from '@/views/common/PageView'
import LoginView from '@/views/login/Common'
import EmptyPageView from '@/views/common/EmptyPageView'
import HomePageView from '@/views/HomePage'
import db from 'utils/localstorage'
import request from 'utils/request'
Vue.use(Router)
let constRouter = [
{
path: '/login',
name: '登录页',
component: LoginView
},
{
path: '/index',
name: '首页',
redirect: '/home'
}
]
let router = new Router({
routes: constRouter
})
const whiteList = ['/login']
let asyncRouter
//
router.beforeEach((to, from, next) => {
if (whiteList.indexOf(to.path) !== -1) {
next()
}
let token = db.get('USER_TOKEN')
let user = db.get('USER')
let userRouter = get('USER_ROUTER')
if (token.length && user) {
if (!asyncRouter) {
if (!userRouter) {
request.get(`menu/${user.username}`).then((res) => {
asyncRouter = res.data
save('USER_ROUTER', asyncRouter)
go(to, next)
})
} else {
asyncRouter = userRouter
go(to, next)
}
} else {
next()
}
} else {
next('/login')
}
})
function go (to, next) {
asyncRouter = filterAsyncRouter(asyncRouter)
router.addRoutes(asyncRouter)
next({...to, replace: true})
}
function save (name, data) {
localStorage.setItem(name, JSON.stringify(data))
}
function get (name) {
return JSON.parse(localStorage.getItem(name))
}
function filterAsyncRouter (routes) {
return routes.filter((route) => {
let component = route.component
if (component) {
switch (route.component) {
case 'MenuView':
route.component = MenuView
break
case 'PageView':
route.component = PageView
break
case 'EmptyPageView':
route.component = EmptyPageView
break
case 'HomePageView':
route.component = HomePageView
break
default:
route.component = view(component)
}
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
return true
}
})
}
function view (path) {
return function (resolve) {
import(`@/views/${path}.vue`).then(mod => {
resolve(mod)
})
}
}
export default router

View File

@ -0,0 +1,13 @@
import Vue from 'vue'
import Vuex from 'vuex'
import account from './modules/account'
import setting from './modules/setting'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
account,
setting
}
})

View File

@ -0,0 +1,34 @@
import db from 'utils/localstorage'
export default {
namespaced: true,
state: {
token: db.get('USER_TOKEN'),
expireTime: db.get('EXPIRE_TIME'),
user: db.get('USER'),
permissions: db.get('PERMISSIONS'),
roles: db.get('ROLES')
},
mutations: {
setToken (state, val) {
db.save('USER_TOKEN', val)
state.token = val
},
setExpireTime (state, val) {
db.save('EXPIRE_TIME', val)
state.expireTime = val
},
setUser (state, val) {
db.save('USER', val)
state.user = val
},
setPermissions (state, val) {
db.save('PERMISSIONS', val)
state.permissions = val
},
setRoles (state, val) {
db.save('ROLES', val)
state.roles = val
}
}
}

View File

@ -0,0 +1,78 @@
import db from 'utils/localstorage'
export default {
namespaced: true,
state: {
sidebar: {
opened: true
},
settingBar: {
opened: false
},
isMobile: false,
theme: db.get('THEME', 'light'),
layout: db.get('LAYOUT', 'side'),
systemName: 'SDN 控制平台',
copyright: `${new Date().getFullYear()} <a href="http://hy.10086.cn/" target="_blank">中移(杭州)信息技术有限公司</a>`,
multipage: getBooleanValue(db.get('MULTIPAGE'), true),
fixSiderbar: getBooleanValue(db.get('FIX_SIDERBAR'), true),
fixHeader: getBooleanValue(db.get('FIX_HEADER'), true),
colorList: [
'rgb(245, 34, 45)',
'rgb(250, 84, 28)',
'rgb(250, 173, 20)',
'rgb(66, 185, 131)',
'rgb(82, 196, 26)',
'rgb(24, 144, 255)',
'rgb(47, 84, 235)',
'rgb(114, 46, 209)'
],
color: db.get('COLOR', 'rgb(24, 144, 255)')
},
mutations: {
setDevice (state, isMobile) {
state.isMobile = isMobile
},
setTheme (state, theme) {
db.save('THEME', theme)
state.theme = theme
},
setLayout (state, layout) {
db.save('LAYOUT', layout)
state.layout = layout
},
setMultipage (state, multipage) {
db.save('MULTIPAGE', multipage)
state.multipage = multipage
},
setSidebar (state, type) {
state.sidebar.opened = type
},
fixSiderbar (state, flag) {
db.save('FIX_SIDERBAR', flag)
state.fixSiderbar = flag
},
fixHeader (state, flag) {
db.save('FIX_HEADER', flag)
state.fixHeader = flag
},
setSettingBar (state, flag) {
state.settingBar.opened = flag
},
setColor (state, color) {
db.save('COLOR', color)
state.color = color
}
}
}
function getBooleanValue (value, defaultValue) {
if (Object.is(value, null)) {
return defaultValue
}
if (JSON.stringify(value) !== '{}') {
return value
} else {
return false
}
}

View File

@ -0,0 +1,57 @@
import { message } from 'ant-design-vue/es'
let lessNodesAppended
const updateTheme = primaryColor => {
if (!primaryColor) {
return
}
const hideMessage = message.loading('加载主题...', 0)
function buildIt () {
if (!window.less) {
return
}
setTimeout(() => {
window.less
.modifyVars({
'@primary-color': primaryColor
})
.then(() => {
hideMessage()
})
.catch((e) => {
console.log(e)
message.error('Failed to update theme')
hideMessage()
})
}, 200)
}
if (!lessNodesAppended) {
// insert less.js and color.less
const lessStyleNode = document.createElement('link')
const lessConfigNode = document.createElement('script')
const lessScriptNode = document.createElement('script')
lessStyleNode.setAttribute('rel', 'stylesheet/less')
lessStyleNode.setAttribute('href', '/static/less/Color.less')
lessConfigNode.innerHTML = `
window.less = {
async: true,
env: 'production',
javascriptEnabled: true
}
`
lessScriptNode.src = 'https://cdn.bootcss.com/less.js/3.9.0/less.min.js'
lessScriptNode.async = true
lessScriptNode.onload = () => {
buildIt()
lessScriptNode.onload = null
}
document.body.appendChild(lessStyleNode)
document.body.appendChild(lessConfigNode)
document.body.appendChild(lessScriptNode)
lessNodesAppended = true
} else {
buildIt()
}
}
export { updateTheme }

View File

@ -0,0 +1,6 @@
export function triggerWindowResizeEvent () {
let event = document.createEvent('HTMLEvents')
event.initEvent('resize', true, true)
event.eventType = 'message'
window.dispatchEvent(event)
}

View File

@ -0,0 +1,15 @@
import enquireJs from 'enquire.js'
const enquireScreen = function (call) {
const hanlder = {
match: function () {
call && call(true)
},
unmatch: function () {
call && call(false)
}
}
enquireJs.register('only screen and (max-width: 767.99px)', hanlder)
}
export default enquireScreen

View File

@ -0,0 +1,17 @@
import Vue from 'vue'
import {hasPermission, hasNoPermission, hasAnyPermission, hasRole, hasAnyRole} from 'utils/permissionDirect'
const Plugins = [
hasPermission,
hasNoPermission,
hasAnyPermission,
hasRole,
hasAnyRole
]
Plugins.map((plugin) => {
Vue.use(plugin)
})
export default Vue

View File

@ -0,0 +1,16 @@
let db = {
save (key, value) {
localStorage.setItem(key, JSON.stringify(value))
},
get (key, defaultValue = {}) {
return JSON.parse(localStorage.getItem(key)) || defaultValue
},
remove (key) {
localStorage.removeItem(key)
},
clear () {
localStorage.clear()
}
}
export default db

View File

@ -0,0 +1,126 @@
// Vue
//
export const hasPermission = {
install (Vue) {
Vue.directive('hasPermission', {
bind (el, binding, vnode) {
let permissions = vnode.context.$store.state.account.permissions
let value = binding.value.split(',')
let flag = true
for (let v of value) {
if (!permissions.includes(v)) {
flag = false
}
}
if (!flag) {
if (!el.parentNode) {
el.style.display = 'none'
} else {
el.parentNode.removeChild(el)
}
}
}
})
}
}
//
export const hasNoPermission = {
install (Vue) {
Vue.directive('hasNoPermission', {
bind (el, binding, vnode) {
let permissions = vnode.context.$store.state.account.permissions
let value = binding.value.split(',')
let flag = true
for (let v of value) {
if (permissions.includes(v)) {
flag = false
}
}
if (!flag) {
if (!el.parentNode) {
el.style.display = 'none'
} else {
el.parentNode.removeChild(el)
}
}
}
})
}
}
//
export const hasAnyPermission = {
install (Vue) {
Vue.directive('hasAnyPermission', {
bind (el, binding, vnode) {
let permissions = vnode.context.$store.state.account.permissions
let value = binding.value.split(',')
let flag = false
for (let v of value) {
if (permissions.includes(v)) {
flag = true
}
}
if (!flag) {
if (!el.parentNode) {
el.style.display = 'none'
} else {
el.parentNode.removeChild(el)
}
}
}
})
}
}
//
export const hasRole = {
install (Vue) {
Vue.directive('hasRole', {
bind (el, binding, vnode) {
let permissions = vnode.context.$store.state.account.roles
let value = binding.value.split(',')
let flag = true
for (let v of value) {
if (!permissions.includes(v)) {
flag = false
}
}
if (!flag) {
if (!el.parentNode) {
el.style.display = 'none'
} else {
el.parentNode.removeChild(el)
}
}
}
})
}
}
//
export const hasAnyRole = {
install (Vue) {
Vue.directive('hasAnyRole', {
bind (el, binding, vnode) {
let permissions = vnode.context.$store.state.account.roles
let value = binding.value.split(',')
let flag = false
for (let v of value) {
if (permissions.includes(v)) {
flag = true
}
}
if (!flag) {
if (!el.parentNode) {
el.style.display = 'none'
} else {
el.parentNode.removeChild(el)
}
}
}
})
}
}

View File

@ -0,0 +1,219 @@
import axios from 'axios'
import {message, Modal, notification} from 'ant-design-vue'
import moment from 'moment'
import store from '../store'
import db from 'utils/localstorage'
moment.locale('zh-cn')
//
let SDN_REQUEST = axios.create({
// baseURL: 'http://127.0.0.1:9527/',
baseURL: 'http://192.168.100.98:8181/',
responseType: 'json',
withCredentials: true,
validateStatus (status) {
// 200
return status === 200
}
})
//
SDN_REQUEST.interceptors.request.use((config) => {
let expireTime = store.state.account.expireTime
let now = moment().format('YYYYMMDDHHmmss')
// token10
if (now - expireTime >= -10) {
Modal.error({
title: '登录已过期',
content: '很抱歉,登录已过期,请重新登录',
okText: '重新登录',
mask: false,
onOk: () => {
return new Promise((resolve, reject) => {
db.clear()
location.reload()
})
}
})
}
// token
if (store.state.account.token) {
config.headers.Authentication = store.state.account.token
}
return config
}, (error) => {
return Promise.reject(error)
})
//
SDN_REQUEST.interceptors.response.use((config) => {
return config
}, (error) => {
if (error.response) {
let errorMessage = error.response.data === null ? '系统内部异常,请联系网站管理员' : error.response.data.message
switch (error.response.status) {
case 404:
notification.error({
message: '系统提示',
description: '很抱歉,资源未找到',
duration: 4
})
break
case 403:
case 401:
notification.warn({
message: '系统提示',
description: '很抱歉,您无法访问该资源,可能是因为没有相应权限或者登录已失效',
duration: 4
})
break
default:
notification.error({
message: '系统提示',
description: errorMessage,
duration: 4
})
break
}
}
return Promise.reject(error)
})
const request = {
post (url, params) {
return SDN_REQUEST.post(url, params, {
transformRequest: [(params) => {
let result = ''
Object.keys(params).forEach((key) => {
if (!Object.is(params[key], undefined) && !Object.is(params[key], null)) {
result += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) + '&'
}
})
return result
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
},
put (url, params) {
return SDN_REQUEST.put(url, params, {
transformRequest: [(params) => {
let result = ''
Object.keys(params).forEach((key) => {
if (!Object.is(params[key], undefined) && !Object.is(params[key], null)) {
result += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) + '&'
}
})
return result
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
},
get (url, params) {
let _params
if (Object.is(params, undefined)) {
_params = ''
} else {
_params = '?'
for (let key in params) {
if (params.hasOwnProperty(key) && params[key] !== null) {
_params += `${key}=${params[key]}&`
}
}
}
return SDN_REQUEST.get(`${url}${_params}`)
},
delete (url, params) {
let _params
if (Object.is(params, undefined)) {
_params = ''
} else {
_params = '?'
for (let key in params) {
if (params.hasOwnProperty(key) && params[key] !== null) {
_params += `${key}=${params[key]}&`
}
}
}
return SDN_REQUEST.delete(`${url}${_params}`)
},
export (url, params = {}) {
message.loading('导出数据中')
return SDN_REQUEST.post(url, params, {
transformRequest: [(params) => {
let result = ''
Object.keys(params).forEach((key) => {
if (!Object.is(params[key], undefined) && !Object.is(params[key], null)) {
result += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) + '&'
}
})
return result
}],
responseType: 'blob'
}).then((r) => {
const content = r.data
const blob = new Blob([content])
const fileName = `${new Date().getTime()}_导出结果.xlsx`
if ('download' in document.createElement('a')) {
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
} else {
navigator.msSaveBlob(blob, fileName)
}
}).catch((r) => {
console.error(r)
message.error('导出失败')
})
},
download (url, params, filename) {
message.loading('文件传输中')
return SDN_REQUEST.post(url, params, {
transformRequest: [(params) => {
let result = ''
Object.keys(params).forEach((key) => {
if (!Object.is(params[key], undefined) && !Object.is(params[key], null)) {
result += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) + '&'
}
})
return result
}],
responseType: 'blob'
}).then((r) => {
const content = r.data
const blob = new Blob([content])
if ('download' in document.createElement('a')) {
const elink = document.createElement('a')
elink.download = filename
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
} else {
navigator.msSaveBlob(blob, filename)
}
}).catch((r) => {
console.error(r)
message.error('下载失败')
})
},
upload (url, params) {
return SDN_REQUEST.post(url, params, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
}
export default request

View File

@ -0,0 +1,48 @@
.textOverflow() {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.textOverflowMulti(@line: 3, @bg: #fff) {
overflow: hidden;
position: relative;
line-height: 1.5em;
max-height: @line * 1.5em;
text-align: justify;
margin-right: -1em;
padding-right: 1em;
&:before {
background: @bg;
content: '...';
padding: 0 1px;
position: absolute;
right: 14px;
bottom: 0;
}
&:after {
background: white;
content: '';
margin-top: 0.2em;
position: absolute;
right: 14px;
width: 1em;
height: 1em;
}
}
.clearfix() {
zoom: 1;
&:before,
&:after {
content: ' ';
display: table;
}
&:after {
clear: both;
visibility: hidden;
font-size: 0;
height: 0;
}
}

View File

@ -0,0 +1,291 @@
<template>
<div :class="[multipage === true ? 'multi-page':'single-page', 'not-menu-page', 'home-page']">
<a-row :gutter="8" class="head-info">
<a-card class="head-info-card">
<a-col :span="12">
<div class="head-info-avatar">
<img alt="头像" :src="avatar">
</div>
<div class="head-info-count">
<div class="head-info-welcome">
{{welcomeMessage}}
</div>
<div class="head-info-desc">
<p>{{user.deptName ? user.deptName : '暂无部门'}} | {{user.roleName ? user.roleName : '暂无角色'}}</p>
</div>
<div class="head-info-time">上次登录时间{{user.lastLoginTime ? user.lastLoginTime : '第一次访问系统'}}</div>
</div>
</a-col>
<a-col :span="12">
<div>
<a-row class="more-info">
<a-col :span="4"></a-col>
<a-col :span="4"></a-col>
<a-col :span="4"></a-col>
<a-col :span="4">
<head-info title="今日IP" :content="todayIp" :center="false" :bordered="false"/>
</a-col>
<a-col :span="4">
<head-info title="今日访问" :content="todayVisitCount" :center="false" :bordered="false"/>
</a-col>
<a-col :span="4">
<head-info title="总访问量" :content="totalVisitCount" :center="false" />
</a-col>
</a-row>
</div>
</a-col>
</a-card>
</a-row>
<a-row :gutter="8" class="count-info">
<a-col :span="12" class="visit-count-wrapper">
<a-card class="visit-count">
<apexchart ref="count" type=bar height=300 :options="chartOptions" :series="series" />
</a-card>
</a-col>
</a-row>
</div>
</template>
<script>
import HeadInfo from '@/views/common/HeadInfo'
import {mapState} from 'vuex'
import moment from 'moment'
moment.locale('zh-cn')
export default {
name: 'HomePage',
components: {HeadInfo},
data () {
return {
series: [],
chartOptions: {
chart: {
toolbar: {
show: false
}
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '35%'
}
},
dataLabels: {
enabled: false
},
stroke: {
show: true,
width: 2,
colors: ['transparent']
},
xaxis: {
categories: []
},
fill: {
opacity: 1
}
},
todayIp: '',
todayVisitCount: '',
totalVisitCount: '',
userRole: '',
userDept: '',
lastLoginTime: '',
welcomeMessage: ''
}
},
computed: {
...mapState({
multipage: state => state.setting.multipage,
user: state => state.account.user
}),
avatar () {
return `static/avatar/${this.user.avatar}`
}
},
methods: {
welcome () {
const date = new Date()
const hour = date.getHours()
let time = hour < 6 ? '早上好' : (hour <= 11 ? '上午好' : (hour <= 13 ? '中午好' : (hour <= 18 ? '下午好' : '晚上好')))
let welcomeArr = [
'喝杯咖啡休息下吧☕',
'要不要和朋友打局LOL',
'要不要和朋友打局王者荣耀',
'几天没见又更好看了呢😍',
'今天吃了什么好吃的呢',
'今天您微笑了吗😊',
'今天帮助别人解决问题了吗',
'准备吃些什么呢',
'周末要不要去看电影?'
]
let index = Math.floor((Math.random() * welcomeArr.length))
return `${time}${this.user.username}${welcomeArr[index]}`
}
},
mounted () {
this.welcomeMessage = this.welcome()
this.$get(`index/${this.user.username}`).then((r) => {
let data = r.data.data
this.todayIp = data.todayIp
this.todayVisitCount = data.todayVisitCount
this.totalVisitCount = data.totalVisitCount
let sevenVisitCount = []
let dateArr = []
for (let i = 6; i >= 0; i--) {
let time = moment().subtract(i, 'days').format('MM-DD')
let contain = false
for (let o of data.lastSevenVisitCount) {
if (o.days === time) {
contain = true
sevenVisitCount.push(o.count)
}
}
if (!contain) {
sevenVisitCount.push(0)
}
dateArr.push(time)
}
let sevenUserVistCount = []
for (let i = 6; i >= 0; i--) {
let time = moment().subtract(i, 'days').format('MM-DD')
let contain = false
for (let o of data.lastSevenUserVisitCount) {
if (o.days === time) {
contain = true
sevenUserVistCount.push(o.count)
}
}
if (!contain) {
sevenUserVistCount.push(0)
}
}
this.$refs.count.updateSeries([
{
name: '您',
data: sevenUserVistCount
},
{
name: '总数',
data: sevenVisitCount
}
], true)
this.$refs.count.updateOptions({
xaxis: {
categories: dateArr
},
title: {
text: '近七日系统访问记录',
align: 'left'
}
}, true, true)
}).catch((r) => {
console.error(r)
this.$message.error('获取首页信息失败')
})
}
}
</script>
<style lang="less">
.home-page {
.head-info {
margin-bottom: .5rem;
.head-info-card {
padding: .5rem;
border-color: #f1f1f1;
.head-info-avatar {
display: inline-block;
float: left;
margin-right: 1rem;
img {
width: 5rem;
border-radius: 2px;
}
}
.head-info-count {
display: inline-block;
float: left;
.head-info-welcome {
font-size: 1.05rem;
margin-bottom: .1rem;
}
.head-info-desc {
color: rgba(0, 0, 0, 0.45);
font-size: .8rem;
padding: .2rem 0;
p {
margin-bottom: 0;
}
}
.head-info-time {
color: rgba(0, 0, 0, 0.45);
font-size: .8rem;
padding: .2rem 0;
}
}
}
}
.count-info {
.visit-count-wrapper {
padding-left: 0 !important;
.visit-count {
padding: .5rem;
border-color: #f1f1f1;
.ant-card-body {
padding: .5rem 1rem !important;
}
}
}
.project-wrapper {
padding-right: 0 !important;
.project-card {
border: none !important;
.ant-card-head {
border-left: 1px solid #f1f1f1 !important;
border-top: 1px solid #f1f1f1 !important;
border-right: 1px solid #f1f1f1 !important;
}
.ant-card-body {
padding: 0 !important;
table {
width: 100%;
td {
width: 50%;
border: 1px solid #f1f1f1;
padding: .6rem;
.project-avatar-wrapper {
display:inline-block;
float:left;
margin-right:.7rem;
.project-avatar {
color: #42b983;
background-color: #d6f8b8;
}
}
}
}
}
.project-detail {
display:inline-block;
float:left;
text-align:left;
width: 78%;
.project-name {
font-size:.9rem;
margin-top:-2px;
font-weight:600;
}
.project-desc {
color:rgba(0, 0, 0, 0.45);
p {
margin-bottom:0;
font-size:.6rem;
white-space:normal;
}
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,8 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'EmptyPageView'
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<div class="footer">
<div class="copyright">
Copyright
<a-icon type="copyright"/>
<span v-html="copyright"></span>
</div>
</div>
</template>
<script>
export default {
name: 'GlobalFooter',
props: ['copyright']
}
</script>
<style lang="less" scoped>
.footer {
padding: 0 16px;
margin: 24px 0;
text-align: center;
.copyright {
color: rgba(0, 0, 0, .45);
font-size: 14px;
i {
font-size: .8rem !important;
}
}
&a {
text-decoration: none;
}
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<a-layout-header :class="[fixHeader && 'ant-header-fixedHeader', layout === 'side' ? (sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed') : null, theme, 'global-header' ]">
<div :class="['global-header-wide', layout]">
<router-link v-if="isMobile || layout === 'head'" to="/" :class="['logo', isMobile ? null : 'pc', theme]">
<img width="32" src="static/img/logo.png" alt=""/>
<h1 v-if="!isMobile">{{systemName}}</h1>
</router-link>
<a-divider v-if="isMobile" type="vertical" />
<a-icon v-if="layout === 'side'" class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggleCollapse"/>
<div v-if="layout === 'head'" class="global-header-menu">
<i-menu style="height: 64px; line-height: 64px;" class="system-top-menu" :theme="theme" mode="horizontal" :menuData="menuData" @select="onSelect"/>
</div>
<div :class="['global-header-right', theme]">
<header-avatar class="header-item"/>
</div>
</div>
</a-layout-header>
</template>
<script>
import HeaderAvatar from './HeaderAvatar'
import IMenu from '@/components/menu/menu'
import { mapState } from 'vuex'
export default {
name: 'GlobalHeader',
components: {IMenu, HeaderAvatar},
props: ['collapsed', 'menuData'],
computed: {
...mapState({
isMobile: state => state.setting.isMobile,
layout: state => state.setting.layout,
systemName: state => state.setting.systemName,
sidebarOpened: state => state.setting.sidebar.opened,
fixHeader: state => state.setting.fixHeader
}),
theme () {
return this.layout === 'side' ? 'light' : this.$store.state.setting.theme
}
},
methods: {
toggleCollapse () {
this.$emit('toggleCollapse')
},
onSelect (obj) {
this.$emit('menuSelect', obj)
}
}
}
</script>
<style lang="less" scoped>
.trigger {
font-size: 20px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color .3s;
}
.header-item{
padding: 0 19px;
display: inline-block;
height: 100%;
cursor: pointer;
vertical-align: middle;
i{
font-size: 16px;
color: rgba(0,0,0,.65);
}
}
.global-header{
padding: 0 12px 0 0;
-webkit-box-shadow: 0 1px 4px rgba(0,21,41,.08);
box-shadow: 0 1px 4px rgba(0,21,41,.08);
position: relative;
&.light{
background: #fff;
}
&.dark{
background: #393e46;
}
.global-header-wide{
&.head{
padding: 0 24px;
}
&.side{
}
.logo {
height: 64px;
line-height: 58px;
vertical-align: top;
display: inline-block;
padding: 0 12px 0 24px;
cursor: pointer;
font-size: 20px;
&.pc{
padding: 0 12px 0 0;
}
img {
display: inline-block;
vertical-align: middle;
}
h1{
display: inline-block;
font-size: 16px;
}
&.dark h1{
color: #fff;
}
}
.global-header-menu{
display: inline-block;
}
.global-header-right{
float: right;
&.dark{
color: #fff;
i{
color: #fff;
}
}
}
}
}
.ant-header-fixedHeader {
position: fixed;
top: 0;
right: 0;
z-index: 15;
width: 100%;
transition: width .2s;
&.ant-header-side-opened {
width: 100%;
padding-left: 254px;
}
&.ant-header-side-closed {
width: 100%;
padding-left: 80px;
}
}
</style>

View File

@ -0,0 +1,100 @@
<template>
<a-layout>
<drawer v-if="isMobile" :openDrawer="collapsed" @change="onDrawerChange">
<sider-menu :theme="theme" :menuData="menuData" :collapsed="false" :collapsible="false" @menuSelect="onMenuSelect"/>
</drawer>
<sider-menu :theme="theme" v-else-if="layout === 'side'" :menuData="menuData" :collapsed="collapsed" :collapsible="true" />
<drawer :open-drawer="settingBar" placement="right">
<setting />
</drawer>
<a-layout :style="{ paddingLeft: paddingLeft }">
<global-header :menuData="menuData" :collapsed="collapsed" @toggleCollapse="toggleCollapse"/>
<a-layout-content :style="{minHeight: minHeight, margin: '20px 14px 0'}" :class="fixHeader ? 'fixed-header-content' : null">
<slot></slot>
</a-layout-content>
<a-layout-footer style="padding: .29rem 0" class="copyright">
<global-footer :copyright="copyright"/>
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script>
import GlobalHeader from './GlobalHeader'
import GlobalFooter from './GlobalFooter'
import Drawer from '~/tool/Drawer'
import SiderMenu from '~/menu/SiderMenu'
import Setting from '~/setting/Setting'
import { mapState, mapMutations } from 'vuex'
import { triggerWindowResizeEvent } from 'utils/common'
const minHeight = window.innerHeight - 64 - 24 - 66
let menuData = []
export default {
name: 'GlobalLayout',
components: {Setting, SiderMenu, Drawer, GlobalFooter, GlobalHeader},
data () {
return {
minHeight: minHeight + 'px',
collapsed: false,
menuData: menuData
}
},
computed: {
paddingLeft () {
return this.fixSiderbar && this.layout === 'side' && !this.isMobile ? `${this.sidebarOpened ? 256 : 80}px` : '0'
},
...mapState({
sidebarOpened: state => state.setting.sidebar.opened,
isMobile: state => state.setting.isMobile,
theme: state => state.setting.theme,
layout: state => state.setting.layout,
copyright: state => state.setting.copyright,
fixSiderbar: state => state.setting.fixSiderbar,
fixHeader: state => state.setting.fixHeader,
settingBar: state => state.setting.settingBar.opened
})
},
methods: {
...mapMutations({setSidebar: 'setting/setSidebar'}),
toggleCollapse () {
this.collapsed = !this.collapsed
this.setSidebar(!this.collapsed)
triggerWindowResizeEvent()
},
onDrawerChange (show) {
this.collapsed = show
},
onMenuSelect () {
this.toggleCollapse()
}
},
beforeCreate () {
let routers = this.$db.get('USER_ROUTER')
menuData = routers.find((item) => item.path === '/').children.filter((menu) => {
let meta = menu.meta
if (typeof meta.isShow === 'undefined') {
return true
} else return meta.isShow
})
}
}
</script>
<style lang="less" scoped>
.setting{
background-color: #1890ff;
color: #fff;
border-radius: 5px 0 0 5px;
line-height: 40px;
font-size: 22px;
width: 40px;
height: 40px;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.fixed-header-content {
margin: 76px 12px 0 !important;
}
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="head-info" :class="center && 'center'">
<span>{{ title }}</span>
<p><a>{{ content }}</a></p>
</div>
</template>
<script>
export default {
name: 'HeadInfo',
props: {
title: {
default: ''
},
content: {
default: ''
},
bordered: {
default: false
},
center: {
default: true
}
}
}
</script>
<style lang="less" scoped>
.head-info {
position: relative;
text-align: left;
padding: 0 32px 0 0;
min-width: 125px;
&.center {
text-align: center;
padding: 0 32px;
}
span {
color: rgba(0, 0, 0, .45);
display: inline-block;
font-size: .95rem;
line-height: 32px;
margin-bottom: 4px;
}
p {
line-height: 32px;
margin: 0;
a {
font-weight: 600;
font-size: 1rem;
}
}
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<div>
<a-dropdown style="display: inline-block; height: 100%; vertical-align: initial">
<span style="cursor: pointer">
<a-avatar class="avatar" size="small" shape="circle"
:src="avatar"/>
<span class="curr-user">{{user.username}}</span>
</span>
<a-menu style="width: 150px" slot="overlay">
<a-menu-item @click="openProfile">
<a-icon type="user"/>
<span>个人中心</span>
</a-menu-item>
<a-menu-item @click="updatePassword">
<a-icon type="key"/>
<span>密码修改</span>
</a-menu-item>
<a-menu-divider></a-menu-divider>
<a-menu-item @click="handleSettingClick">
<a-icon type="setting"/>
<span>系统定制</span>
</a-menu-item>
<a-menu-divider></a-menu-divider>
<a-menu-item @click="logout">
<a-icon type="logout"/>
<span>退出登录</span>
</a-menu-item>
</a-menu>
</a-dropdown>
<update-password
@success="handleUpdate"
@cancel="handleCancelUpdate"
:user="user"
:updatePasswordModelVisible="updatePasswordModelVisible">
</update-password>
</div>
</template>
<script>
import { mapMutations, mapState } from 'vuex'
import UpdatePassword from '../personal/UpdatePassword'
export default {
name: 'HeaderAvatar',
components: {UpdatePassword},
data () {
return {
updatePasswordModelVisible: false
}
},
computed: {
...mapState({
settingBar: state => state.setting.settingBar.opened,
user: state => state.account.user
}),
avatar () {
return `static/avatar/${this.user.avatar}`
}
},
methods: {
handleSettingClick () {
this.setSettingBar(!this.settingBar)
},
openProfile () {
this.$router.push('/profile')
},
updatePassword () {
this.updatePasswordModelVisible = true
},
handleCancelUpdate () {
this.updatePasswordModelVisible = false
},
handleUpdate () {
this.updatePasswordModelVisible = false
this.$message.success('更新密码成功,请重新登录系统')
setTimeout(() => {
this.logout()
}, 1500)
},
logout () {
this.$get(`logout/${this.user.id}`).then(() => {
return new Promise((resolve, reject) => {
this.$db.clear()
location.reload()
})
}).catch(() => {
this.$message.error('退出系统失败')
})
},
...mapMutations({setSettingBar: 'setting/setSettingBar'})
}
}
</script>
<style lang="less" scoped>
.ant-avatar-sm {
width: 30px;
height: 30px;
}
.avatar {
margin: 20px 4px 20px 0;
color: #1890ff;
background: hsla(0, 0%, 100%, .85);
vertical-align: middle;
}
.curr-user {
font-weight: 600;
margin-left: 6px
}
</style>

View File

@ -0,0 +1,161 @@
<template>
<global-layout>
<contextmenu :itemList="menuItemList" :visible.sync="menuVisible" @select="onMenuSelect"/>
<a-tabs
class="page-tabs"
@contextmenu.native="e => onContextmenu(e)"
v-if="multipage"
:active-key="activePage"
style="margin-top: -8px; margin-bottom: 8px"
:hide-add="true"
type="editable-card"
@change="changePage"
@edit="editPage">
<a-tab-pane :id="page.fullPath" :key="page.fullPath" v-for="page in pageList" forceRender>
<span slot="tab" :pagekey="page.fullPath">{{page.name}}</span>
</a-tab-pane>
</a-tabs>
<keep-alive v-if="multipage">
<router-view/>
</keep-alive>
<router-view v-else/>
</global-layout>
</template>
<script>
import GlobalLayout from './GlobalLayout'
import Contextmenu from '~/menu/Contextmenu'
export default {
name: 'MenuView',
components: {Contextmenu, GlobalLayout},
data () {
return {
pageList: [],
linkList: [],
activePage: '',
menuVisible: false,
menuItemList: [
{key: '1', icon: 'arrow-left', text: '关闭左侧'},
{key: '2', icon: 'arrow-right', text: '关闭右侧'},
{key: '3', icon: 'close', text: '关闭其它'}
]
}
},
computed: {
multipage () {
return this.$store.state.setting.multipage
}
},
created () {
this.pageList.push(this.$route)
this.linkList.push(this.$route.fullPath)
this.activePage = this.$route.fullPath
},
watch: {
'$route': function (newRoute, oldRoute) {
this.activePage = newRoute.fullPath
if (!this.multipage) {
this.linkList = [newRoute.fullPath]
this.pageList = [newRoute]
} else if (this.linkList.indexOf(newRoute.fullPath) < 0) {
this.linkList.push(newRoute.fullPath)
this.pageList.push(newRoute)
}
},
'activePage': function (key) {
this.$router.push(key)
},
'multipage': function (newVal, oldVal) {
if (!newVal) {
this.linkList = [this.$route.fullPath]
this.pageList = [this.$route]
}
}
},
methods: {
changePage (key) {
this.activePage = key
},
editPage (key, action) {
this[action](key)
},
remove (key) {
if (this.pageList.length === 1) {
this.$router.push('/')
if (!this.pageList[0].meta.closeable) {
return
}
}
this.pageList = this.pageList.filter(item => item.fullPath !== key)
let index = this.linkList.indexOf(key)
this.linkList = this.linkList.filter(item => item !== key)
index = index >= this.linkList.length ? this.linkList.length - 1 : index
this.activePage = this.linkList[index]
},
onContextmenu (e) {
const pagekey = this.getPageKey(e.target)
if (pagekey !== null) {
e.preventDefault()
this.menuVisible = true
}
},
getPageKey (target, depth) {
depth = depth || 0
if (depth > 2) {
return null
}
let pageKey = target.getAttribute('pagekey')
pageKey = pageKey || (target.previousElementSibling ? target.previousElementSibling.getAttribute('pagekey') : null)
return pageKey || (target.firstElementChild ? this.getPageKey(target.firstElementChild, ++depth) : null)
},
onMenuSelect (key, target) {
let pageKey = this.getPageKey(target)
switch (key) {
case '1':
this.closeLeft(pageKey)
break
case '2':
this.closeRight(pageKey)
break
case '3':
this.closeOthers(pageKey)
break
case '4':
this.closeAll(pageKey)
break
default:
break
}
},
closeOthers (pageKey) {
let index = this.linkList.indexOf(pageKey)
this.linkList = this.linkList.slice(index, index + 1)
this.pageList = this.pageList.slice(index, index + 1)
this.activePage = this.linkList[0]
},
closeLeft (pageKey) {
let index = this.linkList.indexOf(pageKey)
this.linkList = this.linkList.slice(index)
this.pageList = this.pageList.slice(index)
if (this.linkList.indexOf(this.activePage) < 0) {
this.activePage = this.linkList[0]
}
},
closeRight (pageKey) {
let index = this.linkList.indexOf(pageKey)
this.linkList = this.linkList.slice(0, index + 1)
this.pageList = this.pageList.slice(0, index + 1)
if (this.linkList.indexOf(this.activePage < 0)) {
this.activePage = this.linkList[this.linkList.length - 1]
}
}
}
}
</script>
<style scoped>
>>>.ant-tabs-tab{
margin-right: 1px !important;
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div class="page-content">
<div :class="['page-header-wide', layout]">
<div class="breadcrumb">
<a-breadcrumb>
<a-breadcrumb-item :key="item.path" v-for="(item, index) in breadcrumb">
<span v-if="index === 0"><router-link to="/">{{item.name}}</router-link></span>
<span v-else>{{item.name}}</span>
</a-breadcrumb-item>
</a-breadcrumb>
</div>
<div class="detail">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'PageContent',
props: {
title: {
type: String,
required: false
},
breadcrumb: {
type: Array,
required: false
},
logo: {
type: String,
required: false
}
},
computed: {
layout () {
return this.$store.state.setting.layout
}
}
}
</script>
<style lang="less" scoped>
.page-content{
background: #fff;
padding: 14px 22px;
border-left: 1px solid #e8e8e8;
border-right: 1px solid #e8e8e8;
border-bottom: 1px solid #e8e8e8;
.page-header-wide{
.breadcrumb{
margin-bottom: .6rem;
}
.detail{
display: flex;
padding: 0 0 1rem 0;
.row {
display: flex;
width: 100%;
}
.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;
.title{
flex: auto;
font-size: 20px;
font-weight: 500;
color: rgba(0,0,0,.85);
margin-bottom: 16px;
}
.logo{
width: 28px;
height: 28px;
border-radius: 4px;
margin-right: 16px;
}
.content{
margin-bottom: 16px;
flex: auto;
}
.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;
}
}
}
}
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<div :class="multipage === true ? 'multi-page':'single-page'">
<page-content :breadcrumb="breadcrumb" :title="title" :logo="logo">
<slot></slot>
</page-content>
</div>
</template>
<script>
import PageContent from './PageContent'
export default {
name: 'PageLayout',
components: {PageContent},
props: ['logo', 'title'],
data () {
return {
breadcrumb: []
}
},
computed: {
multipage () {
return this.$store.state.setting.multipage
}
},
mounted () {
this.getBreadcrumb()
},
updated () {
this.getBreadcrumb()
},
methods: {
getBreadcrumb () {
this.breadcrumb = this.$route.matched
}
}
}
</script>
<style lang="less" scoped>
.link{
margin-top: 16px;
line-height: 24px;
a{
font-size: 14px;
margin-right: 32px;
i{
font-size: 22px;
margin-right: 8px;
}
}
}
.page-content{
&.side{
margin: 24px 24px 0;
}
&.head{
margin: 24px auto 0;
max-width: 1400px;
}
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<page-layout :title="title">
<keep-alive v-if="multipage">
<router-view ref="page"/>
</keep-alive>
<router-view ref="page" v-else/>
</page-layout>
</template>
<script>
import PageLayout from './PageLayout'
export default {
name: 'PageView',
components: {PageLayout},
data () {
return {
title: ''
}
},
computed: {
multipage () {
return this.$store.state.setting.multipage
}
},
mounted () {
this.getPageHeaderInfo()
},
updated () {
this.getPageHeaderInfo()
},
methods: {
getPageHeaderInfo () {
this.title = this.$route.name
}
}
}
</script>
<style lang="less" scoped>
.extraImg{
margin-top: -60px;
text-align: center;
width: 195px;
img{
width: 100%;
}
}
</style>

View File

@ -0,0 +1,14 @@
<template>
<exception-page type="403" />
</template>
<script>
import ExceptionPage from '~/exception/ExceptionPage'
export default {
components: {ExceptionPage}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,14 @@
<template>
<exception-page type="404" />
</template>
<script>
import ExceptionPage from '~/exception/ExceptionPage'
export default {
components: {ExceptionPage}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,14 @@
<template>
<exception-page type="500" />
</template>
<script>
import ExceptionPage from '~/exception/ExceptionPage'
export default {
components: {ExceptionPage}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,103 @@
<template>
<div class="container">
<div class="content">
<div class="top">
<div class="header">
<img alt="logo" class="logo" src="static/img/logo.png" />
<span class="title">{{systemName}}</span>
</div>
<div class="desc"></div>
</div>
<component :is="componentName" @regist="handleRegist" class="main-content"></component>
</div>
<global-footer :copyright="copyright" />
</div>
</template>
<script>
import GlobalFooter from '../common/GlobalFooter'
import Login from './Login'
export default {
name: 'Common',
components: {GlobalFooter, Login},
data () {
return {
componentName: 'Login'
}
},
computed: {
systemName () {
return this.$store.state.setting.systemName
},
copyright () {
return this.$store.state.setting.copyright
}
},
methods: {
handleRegist (val) {
this.componentName = val
}
}
}
</script>
<style lang="less" scoped>
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: #f0f2f5 url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg') no-repeat center 110px;
background-size: 100%;
.content {
padding: 32px 0;
flex: 1;
@media (min-width: 768px){
padding: 116px 0 10px;
}
.top {
text-align: center;
.header {
height: 50px;
line-height: 50px;
a {
text-decoration: none;
}
.logo {
width: 40px;
height: 19px;
vertical-align: center;
margin-right: 16px;
}
.title {
font-size: 28px;
color: rgba(0,0,0,.85);
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
position: relative;
top: 6px;
}
}
.desc {
font-size: 14px;
color: rgba(0,0,0,.45);
margin-top: 12px;
margin-bottom: 40px;
}
}
.main-content {
width: 368px;
margin: 0 auto;
@media screen and (max-width: 576px) {
width: 95%;
}
@media screen and (max-width: 320px) {
.captcha-button{
font-size: 14px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<div class="login">
<a-form @submit.prevent="doLogin" :autoFormCreate="(form) => this.form = form">
<a-tabs size="large" :tabBarStyle="{textAlign: 'center'}" style="padding: 0 2px;" :activeKey="activeKey"
@change="handleTabsChange">
<a-tab-pane tab="账户密码登录" key="1">
<a-alert type="error" :closable="true" v-show="error" :message="error" showIcon
style="margin-bottom: 24px;"></a-alert>
<a-form-item
fieldDecoratorId="name"
:fieldDecoratorOptions="{rules: [{ required: true, message: '请输入账户名', whitespace: true}]}">
<a-input size="large">
<a-icon slot="prefix" type="user"></a-icon>
</a-input>
</a-form-item>
<a-form-item
fieldDecoratorId="password"
:fieldDecoratorOptions="{rules: [{ required: true, message: '请输入密码', whitespace: true}]}">
<a-input size="large" type="password">
<a-icon slot="prefix" type="lock"></a-icon>
</a-input>
</a-form-item>
</a-tab-pane>
</a-tabs>
<a-form-item>
<a-button :loading="loading" style="width: 100%; margin-top: 4px" size="large" htmlType="submit" type="primary">
登录
</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script>
import {mapMutations} from 'vuex'
export default {
name: 'Login',
data () {
return {
loading: false,
error: '',
activeKey: '1'
}
},
computed: {
systemName () {
return this.$store.state.setting.systemName
},
copyright () {
return this.$store.state.setting.copyright
}
},
created () {
this.$db.clear()
this.$router.options.routes = []
},
methods: {
doLogin () {
if (this.activeKey === '1') {
//
this.form.validateFields(['name', 'password'], (errors, values) => {
if (!errors) {
this.loading = true
let name = this.form.getFieldValue('name')
let password = this.form.getFieldValue('password')
this.$post('oauth2/token', {
username: name,
password: password,
scope: 'sdn',
grant_type: 'password'
}).then((r) => {
let data = r.data.data
data.user = name
data.password = password
this.saveLoginData(data)
setTimeout(() => {
this.loading = false
}, 500)
this.$router.push('/')
}).catch(() => {
setTimeout(() => {
this.loading = false
}, 500)
})
}
})
}
},
handleTabsChange (val) {
this.activeKey = val
},
...mapMutations({
setToken: 'account/setToken',
setExpireTime: 'account/setExpireTime',
setPermissions: 'account/setPermissions',
setRoles: 'account/setRoles',
setUser: 'account/setUser',
setTheme: 'setting/setTheme',
setLayout: 'setting/setLayout',
setMultipage: 'setting/setMultipage',
fixSiderbar: 'setting/fixSiderbar',
fixHeader: 'setting/fixHeader',
setColor: 'setting/setColor'
}),
saveLoginData (data) {
this.setToken(data.access_token)
this.setExpireTime(data.expires_in)
this.setUser(data.user)
this.setPermissions(data.permissions)
this.setRoles(data.roles)
this.setTheme(data.config.theme)
this.setLayout(data.config.layout)
this.setMultipage(data.config.multiPage === '1')
this.fixSiderbar(data.config.fixSiderbar === '1')
this.fixHeader(data.config.fixHeader === '1')
this.setColor(data.config.color)
}
}
}
</script>
<style lang="less" scoped>
.login {
.icon {
font-size: 24px;
color: rgba(0, 0, 0, 0.2);
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
}
</style>

View File

@ -0,0 +1,293 @@
<template>
<div class="user-layout-register">
<a-form ref="formRegister" :autoFormCreate="(form)=>{this.form = form}" id="formRegister">
<a-form-item
fieldDecoratorId="email"
:fieldDecoratorOptions="{rules: [{ required: true, message: '请输入注册账号' }, { validator: this.handleUsernameCheck }], validateTrigger: ['change', 'blur']}">
<a-input size="large" type="text" v-model="username" placeholder="账号"></a-input>
</a-form-item>
<a-popover placement="rightTop" trigger="click" :visible="state.passwordLevelChecked">
<template slot="content">
<div :style="{ width: '240px' }">
<div :class="['user-register', passwordLevelClass]">强度<span>{{ passwordLevelName }}</span></div>
<a-progress :percent="state.percent" :showInfo="false" :strokeColor=" passwordLevelColor "/>
<div style="margin-top: 10px;">
<span>请至少输入 6 个字符请不要使用容易被猜到的密码</span>
</div>
</div>
</template>
<a-form-item
fieldDecoratorId="password"
:fieldDecoratorOptions="{rules: [{ required: true, message: '至少6位密码'}, { validator: this.handlePasswordLevel }], validateTrigger: ['change', 'blur']}">
<a-input size="large" v-model="password" type="password" @click="handlePasswordInputClick" autocomplete="false"
placeholder="至少6位密码"></a-input>
</a-form-item>
</a-popover>
<a-form-item
fieldDecoratorId="password2"
:fieldDecoratorOptions="{rules: [{ required: true, message: '至少6位密码' }, { validator: this.handlePasswordCheck }], validateTrigger: ['change', 'blur']}">
<a-input size="large" type="password" autocomplete="false" placeholder="确认密码"></a-input>
</a-form-item>
<!--
<a-form-item
fieldDecoratorId="mobile"
:fieldDecoratorOptions="{rules: [{ required: true, message: '请输入正确的手机号', pattern: /^1[3456789]\d{9}$/ }, { validator: this.handlePhoneCheck } ], validateTrigger: ['change', 'blur'] }">
<a-input size="large" placeholder="11 位手机号">
<a-select slot="addonBefore" size="large" defaultValue="+86">
<a-select-option value="+86">+86</a-select-option>
<a-select-option value="+87">+87</a-select-option>
</a-select>
</a-input>
</a-form-item>
<a-input-group size="large" compact>
<a-select style="width: 20%" size="large" defaultValue="+86">
<a-select-option value="+86">+86</a-select-option>
<a-select-option value="+87">+87</a-select-option>
</a-select>
<a-input style="width: 80%" size="large" placeholder="11 位手机号"></a-input>
</a-input-group>
<a-row :gutter="16">
<a-col class="gutter-row" :span="16">
<a-form-item
fieldDecoratorId="captcha"
:fieldDecoratorOptions="{rules: [{ required: true, message: '请输入验证码' }], validateTrigger: 'blur'}">
<a-input size="large" type="text" placeholder="验证码">
<a-icon slot="prefix" type='mail' :style="{ color: 'rgba(0,0,0,.25)' }"/>
</a-input>
</a-form-item>
</a-col>
<a-col class="gutter-row" :span="8">
<a-button
class="getCaptcha"
size="large"
:disabled="state.smsSendBtn"
@click.stop.prevent="getCaptcha"
v-text="!state.smsSendBtn && '获取验证码'||(state.time+' s')"></a-button>
</a-col>
</a-row>
-->
<a-form-item>
<a-button
size="large"
type="primary"
htmlType="submit"
class="register-button"
:loading="registerBtn"
@click.stop.prevent="handleSubmit"
:disabled="registerBtn">立即注册
</a-button>
<a class="login" @click="returnLogin">使用已有账户登录</a>
</a-form-item>
</a-form>
</div>
</template>
<script>
const levelNames = {
0: '低',
1: '低',
2: '中',
3: '强'
}
const levelClass = {
0: 'error',
1: 'error',
2: 'warning',
3: 'success'
}
const levelColor = {
0: '#ff0000',
1: '#ff0000',
2: '#ff7e05',
3: '#52c41a'
}
export default {
name: 'Regist',
components: {},
data () {
return {
form: null,
username: '',
password: '',
state: {
time: 60,
smsSendBtn: false,
passwordLevel: 0,
passwordLevelChecked: false,
percent: 10,
progressColor: '#FF0000'
},
registerBtn: false
}
},
computed: {
passwordLevelClass () {
return levelClass[this.state.passwordLevel]
},
passwordLevelName () {
return levelNames[this.state.passwordLevel]
},
passwordLevelColor () {
return levelColor[this.state.passwordLevel]
}
},
methods: {
isMobile () {
return this.$store.state.setting.isMobile
},
handlePasswordLevel (rule, value, callback) {
let level = 0
//
if (/[0-9]/.test(value)) {
level++
}
//
if (/[a-zA-Z]/.test(value)) {
level++
}
//
if (/[^0-9a-zA-Z_]/.test(value)) {
level++
}
this.state.passwordLevel = level
this.state.percent = level * 30
if (level >= 2) {
if (level >= 3) {
this.state.percent = 100
}
callback()
} else {
if (level === 0) {
this.state.percent = 10
}
callback(new Error('密码强度不够'))
}
},
handlePasswordCheck (rule, value, callback) {
let password = this.form.getFieldValue('password')
if (value === undefined) {
callback(new Error('请输入密码'))
}
if (value && password && value.trim() !== password.trim()) {
callback(new Error('两次密码不一致'))
}
callback()
},
handleUsernameCheck (rule, value, callback) {
let username = this.username.trim()
if (username.length) {
if (username.length > 10) {
callback(new Error('用户名不能超过10个字符'))
} else if (username.length < 4) {
callback(new Error('用户名不能少于4个字符'))
} else {
this.$get(`user/check/${username}`).then((r) => {
if (r.data) {
callback()
} else {
this.validateStatus = 'error'
callback(new Error('抱歉,该用户名已存在'))
}
})
}
} else {
callback()
}
},
// handlePhoneCheck (rule, value, callback) {
// callback()
// },
handlePasswordInputClick () {
if (!this.isMobile()) {
this.state.passwordLevelChecked = true
return
}
this.state.passwordLevelChecked = false
},
handleSubmit () {
this.form.validateFields((err, values) => {
if (!err) {
this.$post('regist', {
username: this.username,
password: this.password
}).then(() => {
this.$message.success('注册成功')
this.returnLogin()
}).catch(() => {
this.$message.error('抱歉,注册账号失败')
})
}
})
},
// getCaptcha (e) {
// e.preventDefault()
// let that = this
//
// this.form.validateFields(['mobile'], {force: true},
// (err, values) => {
// if (!err) {
// this.state.smsSendBtn = true
//
// let interval = window.setInterval(() => {
// if (that.state.time-- <= 0) {
// that.state.time = 60
// that.state.smsSendBtn = false
// window.clearInterval(interval)
// }
// }, 1000)
// }
// }
// )
// },
returnLogin () {
this.$emit('regist', 'Login')
}
}
}
</script>
<style lang="less">
.user-register {
&.error {
color: #ff0000;
}
&.warning {
color: #ff7e05;
}
&.success {
color: #52c41a;
}
}
.user-layout-register {
.ant-input-group-addon {
&:first-child {
background-color: #fff;
}
}
& > h3 {
font-size: 16px;
margin-bottom: 20px;
}
.getCaptcha {
display: block;
width: 100%;
height: 40px;
}
.register-button {
width: 50%;
}
.login {
float: right;
line-height: 40px;
}
}
</style>

View File

@ -0,0 +1,152 @@
<template>
<a-card :bordered="false" class="card-area">
<div>
<div class="alert">
<a-alert type="success" :show-icon="true">
<div slot="message">
共追踪到 {{dataSource.length}} 条近期HTTP请求记录
<a style="margin-left: 24px" @click="search">点击刷新</a>
</div>
</a-alert>
</div>
<!-- 表格区域 -->
<a-table :columns="columns"
:dataSource="dataSource"
:pagination="pagination"
:loading="loading"
:scroll="{ x: 900 }"
@change="handleTableChange">
</a-table>
</div>
</a-card>
</template>
<script>
import moment from 'moment'
moment.locale('zh-cn')
export default {
data () {
return {
advanced: false,
dataSource: [],
pagination: {
defaultPageSize: 10,
defaultCurrent: 1,
pageSizeOptions: ['10', '20', '30', '40', '100'],
showQuickJumper: true,
showSizeChanger: true,
showTotal: (total, range) => `显示 ${range[0]} ~ ${range[1]} 条记录,共 ${total} 条记录`
},
loading: false
}
},
computed: {
columns () {
return [{
title: '请求时间',
dataIndex: 'timestamp',
customRender: (text, row, index) => {
return moment(text).format('YYYY-MM-DD HH:mm:ss')
}
}, {
title: '请求方法',
dataIndex: 'request.method',
customRender: (text, row, index) => {
switch (text) {
case 'GET':
return <a-tag color="#87d068">{text}</a-tag>
case 'POST':
return <a-tag color="#2db7f5">{text}</a-tag>
case 'PUT':
return <a-tag color="#ffba5a">{text}</a-tag>
case 'DELETE':
return <a-tag color="#f50">{text}</a-tag>
default:
return text
}
},
filters: [
{ text: 'GET', value: 'GET' },
{ text: 'POST', value: 'POST' },
{ text: 'PUT', value: 'PUT' },
{ text: 'DELETE', value: 'DELETE' }
],
filterMultiple: true,
onFilter: (value, record) => record.request.method.includes(value)
}, {
title: '请求URL',
dataIndex: 'request.uri',
customRender: (text, row, index) => {
return text.split('?')[0]
}
}, {
title: '响应状态',
dataIndex: 'response.status',
customRender: (text, row, index) => {
if (text < 200) {
return <a-tag color="pink">{text}</a-tag>
} else if (text < 201) {
return <a-tag color="green">{text}</a-tag>
} else if (text < 399) {
return <a-tag color="cyan">{text}</a-tag>
} else if (text < 403) {
return <a-tag color="orange">{text}</a-tag>
} else if (text < 501) {
return <a-tag color="red">{text}</a-tag>
} else {
return text
}
}
}, {
title: '请求耗时',
dataIndex: 'timeTaken',
customRender: (text, row, index) => {
if (text < 500) {
return <a-tag color="green">{text} ms</a-tag>
} else if (text < 1000) {
return <a-tag color="cyan">{text} ms</a-tag>
} else if (text < 1500) {
return <a-tag color="orange">{text} ms</a-tag>
} else {
return <a-tag color="red">{text} ms</a-tag>
}
}
}]
}
},
mounted () {
this.fetch()
},
methods: {
search () {
this.fetch()
},
handleTableChange (pagination, filters, sorter) {
this.fetch()
},
fetch () {
this.loading = true
this.$get('actuator/httptrace').then((r) => {
let data = r.data
this.loading = false
let filterData = []
for (let d of data.traces) {
if (d.request.method !== 'OPTIONS' &&
d.request.uri.indexOf('httptrace') === -1) {
filterData.push(d)
}
}
this.dataSource = filterData
})
}
}
}
</script>
<style lang="less" scoped>
@import "../../../static/less/Common";
.alert {
margin-bottom: .5rem;
}
</style>

View File

@ -0,0 +1,223 @@
<template>
<a-skeleton active :loading="loading" :paragraph="{rows: 17}">
<div class="jvm-info">
<div class="alert">
<a-alert type="success" :show-icon="true">
<div slot="message">
数据获取时间 {{this.time}}
<a style="margin-left: 24px" @click="create">点击刷新</a>
</div>
</a-alert>
</div>
<table>
<tr>
<th>参数</th>
<th>描述</th>
<th>当前值</th>
</tr>
<tr>
<td><a-tag color="purple">jvm.memory.max</a-tag></td>
<td>JVM 最大内存</td>
<td>{{jvm.memory.max}} MB</td>
</tr>
<tr>
<td><a-tag color="purple">jvm.memory.committed</a-tag></td>
<td>JVM 可用内存</td>
<td>{{jvm.memory.committed}} MB</td>
</tr>
<tr>
<td><a-tag color="purple">jvm.memory.used</a-tag></td>
<td>JVM 已用内存</td>
<td>{{jvm.memory.used}} MB</td>
</tr>
<tr>
<td><a-tag color="cyan">jvm.buffer.memory.used</a-tag></td>
<td>JVM 缓冲区已用内存</td>
<td>{{jvm.buffer.memory.used}} MB</td>
</tr>
<tr>
<td><a-tag color="cyan">jvm.buffer.count</a-tag></td>
<td>当前缓冲区数量</td>
<td>{{jvm.buffer.count}} </td>
</tr>
<tr>
<td><a-tag color="green">jvm.threads.daemon</a-tag></td>
<td>JVM 守护线程数量</td>
<td>{{jvm.threads.daemon}} </td>
</tr>
<tr>
<td><a-tag color="green">jvm.threads.live</a-tag></td>
<td>JVM 当前活跃线程数量</td>
<td>{{jvm.threads.live}} </td>
</tr>
<tr>
<td><a-tag color="green">jvm.threads.peak</a-tag></td>
<td>JVM 峰值线程数量</td>
<td>{{jvm.threads.peak}} </td>
</tr>
<tr>
<td><a-tag color="orange">jvm.classes.loaded</a-tag></td>
<td>JVM 已加载 Class 数量</td>
<td>{{jvm.classes.loaded}} </td>
</tr>
<tr>
<td><a-tag color="orange">jvm.classes.unloaded</a-tag></td>
<td>JVM 未加载 Class 数量</td>
<td>{{jvm.classes.unloaded}} </td>
</tr>
<tr>
<td><a-tag color="pink">jvm.gc.memory.allocated</a-tag></td>
<td>GC , 年轻代分配的内存空间</td>
<td>{{jvm.gc.memory.allocated}} MB</td>
</tr>
<tr>
<td><a-tag color="pink">jvm.gc.memory.promoted</a-tag></td>
<td>GC , 老年代分配的内存空间</td>
<td>{{jvm.gc.memory.promoted}} MB</td>
</tr>
<tr>
<td><a-tag color="pink">jvm.gc.max.data.size</a-tag></td>
<td>GC , 老年代的最大内存空间</td>
<td>{{jvm.gc.maxDataSize}} MB</td>
</tr>
<tr>
<td><a-tag color="pink">jvm.gc.live.data.size</a-tag></td>
<td>FullGC , 老年代的内存空间</td>
<td>{{jvm.gc.liveDataSize}} MB</td>
</tr>
<tr>
<td><a-tag color="blue">jvm.gc.pause.count</a-tag></td>
<td>系统启动以来GC 次数</td>
<td>{{jvm.gc.pause.count}} </td>
</tr>
<tr>
<td><a-tag color="blue">jvm.gc.pause.totalTime</a-tag></td>
<td>系统启动以来GC 总耗时</td>
<td>{{jvm.gc.pause.totalTime}} </td>
</tr>
</table>
</div>
</a-skeleton>
</template>
<script>
import axios from 'axios'
import moment from 'moment'
moment.locale('zh-cn')
export default {
data () {
return {
time: '',
loading: true,
jvm: {
memory: {
max: 0,
committed: 0,
used: 0
},
buffer: {
memory: {
used: 0
},
count: 0
},
threads: {
daemon: 0,
live: 0,
peak: 0
},
classes: {
loaded: 0,
unloaded: 0
},
gc: {
memory: {
allocated: 0,
promoted: 0
},
maxDataSize: 0,
liveDataSize: 0,
pause: {
totalTime: 0,
count: 0
}
}
}
}
},
mounted () {
this.create()
},
methods: {
create () {
this.time = moment().format('YYYY年MM月DD日 HH时mm分ss秒')
axios.all([
this.$get('actuator/metrics/jvm.memory.max'),
this.$get('actuator/metrics/jvm.memory.committed'),
this.$get('actuator/metrics/jvm.memory.used'),
this.$get('actuator/metrics/jvm.buffer.memory.used'),
this.$get('actuator/metrics/jvm.buffer.count'),
this.$get('actuator/metrics/jvm.threads.daemon'),
this.$get('actuator/metrics/jvm.threads.live'),
this.$get('actuator/metrics/jvm.threads.peak'),
this.$get('actuator/metrics/jvm.classes.loaded'),
this.$get('actuator/metrics/jvm.classes.unloaded'),
this.$get('actuator/metrics/jvm.gc.memory.allocated'),
this.$get('actuator/metrics/jvm.gc.memory.promoted'),
this.$get('actuator/metrics/jvm.gc.max.data.size'),
this.$get('actuator/metrics/jvm.gc.live.data.size'),
this.$get('actuator/metrics/jvm.gc.pause')
]).then((r) => {
this.jvm.memory.max = this.convert(r[0].data.measurements[0].value)
this.jvm.memory.committed = this.convert(r[1].data.measurements[0].value)
this.jvm.memory.used = this.convert(r[2].data.measurements[0].value)
this.jvm.buffer.memory.used = this.convert(r[3].data.measurements[0].value)
this.jvm.buffer.count = r[4].data.measurements[0].value
this.jvm.threads.daemon = r[5].data.measurements[0].value
this.jvm.threads.live = r[6].data.measurements[0].value
this.jvm.threads.peak = r[7].data.measurements[0].value
this.jvm.classes.loaded = r[8].data.measurements[0].value
this.jvm.classes.unloaded = r[9].data.measurements[0].value
this.jvm.gc.memory.allocated = this.convert(r[10].data.measurements[0].value)
this.jvm.gc.memory.promoted = this.convert(r[11].data.measurements[0].value)
this.jvm.gc.maxDataSize = this.convert(r[12].data.measurements[0].value)
this.jvm.gc.liveDataSize = this.convert(r[13].data.measurements[0].value)
this.jvm.gc.pause.count = r[14].data.measurements[0].value
this.jvm.gc.pause.totalTime = r[14].data.measurements[1].value
this.loading = false
}).catch((r) => {
console.error(r)
this.$message.error('获取JVM信息失败')
})
},
convert (value) {
return Number(value / 1048576).toFixed(3)
}
}
}
</script>
<style lang="less">
.jvm-info {
width: 100%;
table {
width: 100%;
tr {
line-height: 1.5rem;
border-bottom: 1px solid #f1f1f1;
th {
background: #fafafa;
padding: .5rem;
}
td {
padding: .5rem;
.ant-tag {
font-size: .8rem !important;
}
}
}
}
.alert {
margin-bottom: .5rem;
}
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<a-card :bordered="false" class="card-area">
<div :class="advanced ? 'search' : null">
<!-- 搜索区域 -->
<a-form layout="horizontal">
<div :class="advanced ? null: 'fold'">
<a-row >
<a-col :md="12" :sm="24" >
<a-form-item
label="用户名"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<a-input v-model="queryParams.username"/>
</a-form-item>
</a-col>
</a-row>
</div>
<span style="float: right; margin-top: 3px;">
<a-button type="primary" @click="search">查询</a-button>
<a-button style="margin-left: 8px" @click="reset">重置</a-button>
</span>
</a-form>
</div>
<div>
<!-- 表格区域 -->
<a-table :columns="columns"
:dataSource="dataSource"
:pagination="pagination"
:loading="loading"
:scroll="{ x: 900 }"
@change="handleTableChange">
<template slot="username" slot-scope="text, record">
<template v-if="record.id === user.id">
{{record.username}}&nbsp;&nbsp;<a-tag color="pink">current</a-tag>
</template>
<template v-else>
{{record.username}}
</template>
</template>
<template slot="operation" slot-scope="text, record">
<a-icon v-hasPermission="'user:kickout'" type="poweroff" style="color: #f95476" @click="kickout(record)" title="踢出"></a-icon>
<a-badge v-hasNoPermission="'user:kickout'" status="warning" text="无权限"></a-badge>
</template>
</a-table>
</div>
</a-card>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'Online',
data () {
return {
advanced: false,
dataSource: [],
queryParams: {},
pagination: {
defaultPageSize: 10000000,
hideOnSinglePage: true,
indentSize: 100
},
loading: false
}
},
computed: {
columns () {
return [{
title: '用户名',
dataIndex: 'username',
scopedSlots: { customRender: 'username' }
}, {
title: '登录时间',
dataIndex: 'loginTime'
}, {
title: '登录IP',
dataIndex: 'ip'
}, {
title: '登录地点',
dataIndex: 'loginAddress'
}, {
title: '操作',
dataIndex: 'operation',
scopedSlots: { customRender: 'operation' },
fixed: 'right',
width: 120
}]
},
...mapState({
user: state => state.account.user
})
},
mounted () {
this.fetch()
},
methods: {
search () {
this.fetch({
...this.queryParams
})
},
kickout (record) {
let that = this
this.$confirm({
title: '确定踢出该用户?',
content: '当您点击确定按钮后,该用户的登录将会马上失效',
centered: true,
onOk () {
that.$delete(`kickout/${record.id}`).then(() => {
that.$message.success('踢出用户成功')
if (that.user.id === record.id) {
that.$db.clear()
location.reload()
} else {
that.search()
}
}).catch((r) => {
console.error(r)
that.$message.error('踢出用户失败')
})
}
})
},
reset () {
//
this.queryParams = {}
this.fetch()
},
handleTableChange (pagination, filters, sorter) {
this.fetch({
...this.queryParams
})
},
fetch (params = {}) {
this.loading = true
this.$get('online', {
...params
}).then((r) => {
let data = r.data.data
this.loading = false
this.dataSource = data
})
}
}
}
</script>
<style lang="less" scoped>
@import "../../../static/less/Common";
</style>

View File

@ -0,0 +1,212 @@
<template>
<div style="width: 100%;margin-top: 1rem">
<a-row :gutter="8">
<a-col :span="12">
<apexchart ref="memoryInfo" type=area height=350 :options="memory.chartOptions" :series="memory.series" />
</a-col>
<a-col :span="12">
<apexchart ref="keySize" type=area height=350 :options="key.chartOptions" :series="key.series" />
</a-col>
</a-row>
<a-row :gutter="8">
<a-divider orientation="left">Redis详细信息</a-divider>
<table style="border-bottom: 1px solid #f1f1f1;">
<tr v-for="(info, index) in redisInfo" :key="index" style="border-top: 1px solid #f1f1f1;">
<td style="padding: .7rem 1rem">{{info.key}}</td>
<td style="padding: .7rem 1rem">{{info.description}}</td>
<td style="padding: .7rem 1rem">{{info.value}}</td>
</tr>
</table>
</a-row>
</div>
</template>
<script>
import axios from 'axios'
import moment from 'moment'
export default {
name: 'RedisInfo',
data () {
return {
loading: true,
memory: {
series: [],
chartOptions: {
chart: {
animations: {
enabled: true,
easing: 'linear',
dynamicAnimation: {
speed: 3000
}
},
toolbar: {
show: false
},
zoom: {
enabled: false
}
},
dataLabels: {
enabled: false
},
stroke: {
curve: 'smooth'
},
title: {
text: 'Redis内存实时占用情况kb',
align: 'left'
},
markers: {
size: 0
},
xaxis: {
},
yaxis: {},
legend: {
show: false
}
},
data: [],
xdata: []
},
key: {
series: [],
chartOptions: {
chart: {
animations: {
enabled: true,
easing: 'linear',
dynamicAnimation: {
speed: 3000
}
},
toolbar: {
show: false
},
zoom: {
enabled: false
}
},
dataLabels: {
enabled: false
},
colors: ['#f5564e'],
stroke: {
curve: 'smooth'
},
title: {
text: 'Redis key实时数量',
align: 'left'
},
markers: {
size: 0
},
xaxis: {
},
yaxis: {},
legend: {
show: false
}
},
data: [],
xdata: []
},
redisInfo: [],
timer: null
}
},
beforeDestroy () {
if (this.timer) {
clearInterval(this.timer)
}
},
mounted () {
let minMemory = 1e10
let minSize = 1e10
let maxMemory = -1e10
let maxSize = -1e10
this.timer = setInterval(() => {
if (this.$route.path.indexOf('redis') !== -1) {
axios.all([
this.$get('redis/keysSize'),
this.$get('redis/memoryInfo')
]).then((r) => {
let currentMemory = r[1].data.used_memory / 1000
let currentSize = r[0].data.dbSize
if (currentMemory < minMemory) {
minMemory = currentMemory
}
if (currentMemory > maxMemory) {
maxMemory = currentMemory
}
if (currentSize < minSize) {
minSize = currentSize
}
if (currentSize > maxSize) {
maxSize = currentSize
}
let time = moment().format('hh:mm:ss')
this.memory.data.push(currentMemory)
this.memory.xdata.push(time)
this.key.data.push(currentSize)
this.key.xdata.push(time)
if (this.memory.data.length >= 6) {
this.memory.data.shift()
this.memory.xdata.shift()
}
if (this.key.data.length >= 6) {
this.key.data.shift()
this.key.xdata.shift()
}
this.$refs.memoryInfo.updateSeries([
{
name: '内存(kb)',
data: this.memory.data.slice()
}
])
this.$refs.memoryInfo.updateOptions({
xaxis: {
categories: this.memory.xdata.slice()
},
yaxis: {
min: minMemory,
max: maxMemory
}
}, true, true)
this.$refs.keySize.updateSeries([
{
name: 'key数量',
data: this.key.data.slice()
}
])
this.$refs.keySize.updateOptions({
xaxis: {
categories: this.key.xdata.slice()
},
yaxis: {
min: minSize - 2,
max: maxSize + 2
}
}, true, true)
if (this.loading) {
this.loading = false
}
}).catch((r) => {
console.error(r)
this.$message.error('获取Redis信息失败')
if (this.timer) {
clearInterval(this.timer)
}
})
}
}, 3000)
this.$get('redis/info').then((r) => {
this.redisInfo = r.data.data
})
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,13 @@
<template>
<div>
<div>Redis终端</div>
</div>
</template>
<script>
export default {
name: 'RedisTerminal'
}
</script>
<style>
</style>

View File

@ -0,0 +1,124 @@
<template>
<a-skeleton active :loading="loading" :paragraph="{rows: 17}">
<div class="jvm-info">
<div class="alert">
<a-alert type="success" :show-icon="true">
<div slot="message">
数据获取时间 {{this.time}}
<a style="margin-left: 24px" @click="create">点击刷新</a>
</div>
</a-alert>
</div>
<table>
<tr>
<th>参数</th>
<th>描述</th>
<th>当前值</th>
</tr>
<tr>
<td><a-tag color="green">system.cpu.count</a-tag></td>
<td>CPU 数量</td>
<td>{{system.cpu.count}} </td>
</tr>
<tr>
<td><a-tag color="green">system.cpu.usage</a-tag></td>
<td>系统 CPU 使用率</td>
<td>{{system.cpu.usage}} %</td>
</tr>
<tr>
<td><a-tag color="purple">process.start.time</a-tag></td>
<td>应用启动时间点</td>
<td>{{system.process.startTime}}</td>
</tr>
<tr>
<td><a-tag color="purple">process.uptime</a-tag></td>
<td>应用已运行时间</td>
<td>{{system.process.uptime}} </td>
</tr>
<tr>
<td><a-tag color="purple">process.cpu.usage</a-tag></td>
<td>当前应用 CPU 使用率</td>
<td>{{system.process.cpuUsage}} %</td>
</tr>
</table>
</div>
</a-skeleton>
</template>
<script>
import axios from 'axios'
import moment from 'moment'
moment.locale('zh-cn')
export default {
data () {
return {
time: '',
loading: true,
system: {
cpu: {
count: 0,
usage: 0
},
process: {
cpuUsage: 0,
uptime: 0,
startTime: 0
}
}
}
},
mounted () {
this.create()
},
methods: {
create () {
this.time = moment().format('YYYY年MM月DD日 HH时mm分ss秒')
axios.all([
this.$get('actuator/metrics/system.cpu.count'),
this.$get('actuator/metrics/system.cpu.usage'),
this.$get('actuator/metrics/process.uptime'),
this.$get('actuator/metrics/process.start.time'),
this.$get('actuator/metrics/process.cpu.usage')
]).then((r) => {
this.system.cpu.count = r[0].data.measurements[0].value
this.system.cpu.usage = this.convert(r[1].data.measurements[0].value)
this.system.process.uptime = r[2].data.measurements[0].value
this.system.process.startTime = moment(r[3].data.measurements[0].value * 1000).format('YYYY-MM-DD HH:mm:ss')
this.system.process.cpuUsage = this.convert(r[4].data.measurements[0].value)
this.loading = false
}).catch((r) => {
console.error(r)
this.$message.error('获取服务器信息失败')
})
},
convert (value) {
return Number(value * 100).toFixed(2)
}
}
}
</script>
<style lang="less">
.jvm-info {
width: 100%;
table {
width: 100%;
tr {
line-height: 1.5rem;
border-bottom: 1px solid #f1f1f1;
th {
background: #fafafa;
padding: .5rem;
}
td {
padding: .5rem;
.ant-tag {
font-size: .8rem !important;
}
}
}
}
.alert {
margin-bottom: .5rem;
}
}
</style>

View File

@ -0,0 +1,300 @@
<template>
<a-card :bordered="false" class="card-area">
<a-form layout="horizontal">
<div :class="advanced ? null: 'fold'">
<a-row>
<a-col :md="12" :sm="24" >
<a-form-item
label="操作人"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<a-input v-model="queryParams.username"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" >
<a-form-item
label="操作描述"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<a-input v-model="queryParams.operation"/>
</a-form-item>
</a-col>
</a-row>
<a-row v-if="advanced">
<a-col :md="12" :sm="24" >
<a-form-item
label="操作地点"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<a-input v-model="queryParams.location"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" >
<a-form-item
label="操作时间"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<range-date @change="handleDateChange" ref="createTime" style="width: 100%"></range-date>
</a-form-item>
</a-col>
</a-row>
</div>
<span style="float: right; margin-top: 3px;">
<a-button type="primary" @click="search">查询</a-button>
<a-button style="margin-left: 8px" @click="reset">重置</a-button>
<a @click="toggleAdvanced" style="margin-left: 8px">
{{advanced ? '收起' : '展开'}}
<a-icon :type="advanced ? 'up' : 'down'" />
</a>
</span>
</a-form>
<div>
<div class="operator">
<a-button v-hasPermission="'log:delete'" @click="batchDelete" type="primary" ghost>删除</a-button>
<a-dropdown v-hasPermission="'log:export'">
<a-menu slot="overlay">
<a-menu-item key="export-data" @click="exprotExccel">导出Excel</a-menu-item>
</a-menu>
<a-button>
更多操作 <a-icon type="down" />
</a-button>
</a-dropdown>
</div>
<!-- 表格区域 -->
<a-table ref="TableInfo"
:columns="columns"
:dataSource="dataSource"
:pagination="pagination"
:loading="loading"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
@change="handleTableChange" :scroll="{ x: 1400 }">
<template slot="method" slot-scope="text, record">
<a-popover placement="topLeft">
<template slot="content">
<div>{{text}}</div>
</template>
<p style="width: 200px;margin-bottom: 0">{{text}}</p>
</a-popover>
</template>
<template slot="params" slot-scope="text, record">
<a-popover placement="topLeft">
<template slot="content">
<div style="max-width: 300px;">{{text}}</div>
</template>
<p style="width: 100px;margin-bottom: 0">{{text}}</p>
</a-popover>
</template>
</a-table>
</div>
</a-card>
</template>
<script>
import RangeDate from '@/components/datetime/RangeDate'
export default {
name: 'SystemLog',
components: {RangeDate},
data () {
return {
advanced: false,
dataSource: [],
sortedInfo: null,
paginationInfo: null,
selectedRowKeys: [],
queryParams: {},
pagination: {
pageSizeOptions: ['10', '20', '30', '40', '100'],
defaultCurrent: 1,
defaultPageSize: 10,
showQuickJumper: true,
showSizeChanger: true,
showTotal: (total, range) => `显示 ${range[0]} ~ ${range[1]} 条记录,共 ${total} 条记录`
},
loading: false
}
},
computed: {
columns () {
let { sortedInfo } = this
sortedInfo = sortedInfo || {}
return [{
title: '操作人',
dataIndex: 'username'
}, {
title: '操作描述',
dataIndex: 'operation'
}, {
title: '耗时',
dataIndex: 'time',
customRender: (text, row, index) => {
if (text < 500) {
return <a-tag color="green">{text} ms</a-tag>
} else if (text < 1000) {
return <a-tag color="cyan">{text} ms</a-tag>
} else if (text < 1500) {
return <a-tag color="orange">{text} ms</a-tag>
} else {
return <a-tag color="red">{text} ms</a-tag>
}
},
sorter: true,
sortOrder: sortedInfo.columnKey === 'time' && sortedInfo.order
}, {
title: '执行方法',
dataIndex: 'method',
scopedSlots: { customRender: 'method' }
}, {
title: '方法参数',
dataIndex: 'params',
scopedSlots: { customRender: 'params' },
width: 100
}, {
title: 'IP地址',
dataIndex: 'ip'
}, {
title: '操作地点',
dataIndex: 'location'
}, {
title: '操作时间',
dataIndex: 'createTime',
sorter: true,
sortOrder: sortedInfo.columnKey === 'createTime' && sortedInfo.order
}]
}
},
mounted () {
this.fetch()
},
methods: {
onSelectChange (selectedRowKeys) {
this.selectedRowKeys = selectedRowKeys
},
toggleAdvanced () {
this.advanced = !this.advanced
if (!this.advanced) {
this.queryParams.createTimeFrom = ''
this.queryParams.createTimeTo = ''
this.queryParams.location = ''
}
},
handleDateChange (value) {
if (value) {
this.queryParams.createTimeFrom = value[0]
this.queryParams.createTimeTo = value[1]
}
},
batchDelete () {
if (!this.selectedRowKeys.length) {
this.$message.warning('请选择需要删除的记录')
return
}
let that = this
this.$confirm({
title: '确定删除所选中的记录?',
content: '当您点击确定按钮后,这些记录将会被彻底删除',
centered: true,
onOk () {
let ids = []
for (let key of that.selectedRowKeys) {
ids.push(that.dataSource[key].id)
}
that.$delete('log/' + ids.join(',')).then(() => {
that.$message.success('删除成功')
that.selectedRowKeys = []
that.search()
})
},
onCancel () {
that.selectedRowKeys = []
}
})
},
exprotExccel () {
let {sortedInfo} = this
let sortField, sortOrder
//
if (sortedInfo) {
sortField = sortedInfo.field
sortOrder = sortedInfo.order
}
this.$export('log/excel', {
sortField: sortField,
sortOrder: sortOrder,
...this.queryParams
})
},
search () {
let {sortedInfo} = this
let sortField, sortOrder
//
if (sortedInfo) {
sortField = sortedInfo.field
sortOrder = sortedInfo.order
}
this.fetch({
sortField: sortField,
sortOrder: sortOrder,
...this.queryParams
})
},
reset () {
//
this.selectedRowKeys = []
//
this.$refs.TableInfo.pagination.current = this.pagination.defaultCurrent
if (this.paginationInfo) {
this.paginationInfo.current = this.pagination.defaultCurrent
this.paginationInfo.pageSize = this.pagination.defaultPageSize
}
//
this.sortedInfo = null
//
this.queryParams = {}
//
if (this.advanced) {
this.$refs.createTime.reset()
}
this.fetch()
},
handleTableChange (pagination, filters, sorter) {
// Vue data使
this.paginationInfo = pagination
this.sortedInfo = sorter
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
...this.queryParams,
...filters
})
},
fetch (params = {}) {
this.loading = true
if (this.paginationInfo) {
//
this.$refs.TableInfo.pagination.current = this.paginationInfo.current
this.$refs.TableInfo.pagination.pageSize = this.paginationInfo.pageSize
params.pageSize = this.paginationInfo.pageSize
params.pageNum = this.paginationInfo.current
} else {
//
params.pageSize = this.pagination.defaultPageSize
params.pageNum = this.pagination.defaultCurrent
}
this.$get('log', {
...params
}).then((r) => {
let data = r.data
const pagination = { ...this.pagination }
pagination.total = data.total
this.loading = false
this.dataSource = data.rows
this.pagination = pagination
})
}
}
}
</script>
<style lang="less" scoped>
@import "../../../static/less/Common";
</style>

View File

@ -0,0 +1,201 @@
<template>
<a-skeleton active :loading="loading" :paragraph="{rows: 17}">
<div class="jvm-info">
<div class="alert">
<a-alert type="success" :show-icon="true">
<div slot="message">
数据获取时间 {{this.time}}
<a style="margin-left: 24px" @click="create">点击刷新</a>
</div>
</a-alert>
</div>
<table>
<tr>
<th>参数</th>
<th>描述</th>
<th>当前值</th>
</tr>
<tr>
<td><a-tag color="green">tomcat.sessions.created</a-tag></td>
<td>tomcat 已创建 session </td>
<td>{{tomcat.sessions.created}} </td>
</tr>
<tr>
<td><a-tag color="green">tomcat.sessions.expired</a-tag></td>
<td>tomcat 已过期 session </td>
<td>{{tomcat.sessions.expired}} </td>
</tr>
<tr>
<td><a-tag color="green">tomcat.sessions.active.current</a-tag></td>
<td>tomcat 当前活跃 session </td>
<td>{{tomcat.sessions.active.current}} </td>
</tr>
<tr>
<td><a-tag color="green">tomcat.sessions.active.max</a-tag></td>
<td>tomcat 活跃 session 数峰值</td>
<td>{{tomcat.sessions.active.max}} </td>
</tr>
<tr>
<td><a-tag color="green">tomcat.sessions.rejected</a-tag></td>
<td>超过session 最大配置后拒绝的 session 个数</td>
<td>{{tomcat.sessions.rejected}} </td>
</tr>
<tr>
<td><a-tag color="purple">tomcat.global.sent</a-tag></td>
<td>发送的字节数</td>
<td>{{tomcat.global.sent}} bytes</td>
</tr>
<tr>
<td><a-tag color="purple">tomcat.global.request.max</a-tag></td>
<td>request 请求最长耗时</td>
<td>{{tomcat.global.request.max}} </td>
</tr>
<tr>
<td><a-tag color="purple">tomcat.global.request.count</a-tag></td>
<td>全局 request 请求次数</td>
<td>{{tomcat.global.request.count}} </td>
</tr>
<tr>
<td><a-tag color="purple">tomcat.global.request.totalTime</a-tag></td>
<td>全局 request 请求总耗时</td>
<td>{{tomcat.global.request.totalTime}} </td>
</tr>
<tr>
<td><a-tag color="cyan">tomcat.servlet.request.max</a-tag></td>
<td>servlet 请求最长耗时</td>
<td>{{tomcat.servlet.request.max}} </td>
</tr>
<tr>
<td><a-tag color="cyan">tomcat.servlet.request.count</a-tag></td>
<td>servlet 总请求次数</td>
<td>{{tomcat.servlet.request.count}} </td>
</tr>
<tr>
<td><a-tag color="cyan">tomcat.servlet.request.totalTime</a-tag></td>
<td>servlet 请求总耗时</td>
<td>{{tomcat.servlet.request.totalTime}} </td>
</tr>
<tr>
<td><a-tag color="pink">tomcat.threads.current</a-tag></td>
<td>tomcat 当前线程数包括守护线程</td>
<td>{{tomcat.threads.current}} </td>
</tr>
<tr>
<td><a-tag color="pink">tomcat.threads.configMax</a-tag></td>
<td>tomcat 配置的线程最大数</td>
<td>{{tomcat.threads.configMax}} </td>
</tr>
</table>
</div>
</a-skeleton>
</template>
<script>
import axios from 'axios'
import moment from 'moment'
moment.locale('zh-cn')
export default {
data () {
return {
time: '',
loading: true,
tomcat: {
sessions: {
created: 0,
expired: 0,
active: {
current: 0,
max: 0
},
rejected: 0
},
global: {
sent: 0,
request: {
count: 0,
max: 0,
totalTime: 0
}
},
servlet: {
request: {
count: 0,
totalTime: 0,
max: 0
}
},
threads: {
current: 0,
configMax: 0
}
}
}
},
mounted () {
this.create()
},
methods: {
create () {
this.time = moment().format('YYYY年MM月DD日 HH时mm分ss秒')
axios.all([
this.$get('actuator/metrics/tomcat.sessions.created'),
this.$get('actuator/metrics/tomcat.sessions.expired'),
this.$get('actuator/metrics/tomcat.sessions.active.current'),
this.$get('actuator/metrics/tomcat.sessions.active.max'),
this.$get('actuator/metrics/tomcat.sessions.rejected'),
this.$get('actuator/metrics/tomcat.global.sent'),
this.$get('actuator/metrics/tomcat.global.request.max'),
this.$get('actuator/metrics/tomcat.global.request'),
this.$get('actuator/metrics/tomcat.servlet.request'),
this.$get('actuator/metrics/tomcat.servlet.request.max'),
this.$get('actuator/metrics/tomcat.threads.current'),
this.$get('actuator/metrics/tomcat.threads.config.max')
]).then((r) => {
this.tomcat.sessions.created = r[0].data.measurements[0].value
this.tomcat.sessions.expired = r[1].data.measurements[0].value
this.tomcat.sessions.active.current = r[2].data.measurements[0].value
this.tomcat.sessions.active.max = r[3].data.measurements[0].value
this.tomcat.sessions.rejected = r[4].data.measurements[0].value
this.tomcat.global.sent = r[5].data.measurements[0].value
this.tomcat.global.request.max = r[6].data.measurements[0].value
this.tomcat.global.request.count = r[7].data.measurements[0].value
this.tomcat.global.request.totalTime = r[7].data.measurements[1].value
this.tomcat.servlet.request.count = r[8].data.measurements[0].value
this.tomcat.servlet.request.totalTime = r[8].data.measurements[1].value
this.tomcat.servlet.request.max = r[9].data.measurements[0].value
this.tomcat.threads.current = r[10].data.measurements[0].value
this.tomcat.threads.configMax = r[11].data.measurements[0].value
this.loading = false
}).catch((r) => {
console.error(r)
this.$message.error('获取Tomcat信息失败')
})
}
}
}
</script>
<style lang="less">
.jvm-info {
width: 100%;
table {
width: 100%;
tr {
line-height: 1.5rem;
border-bottom: 1px solid #f1f1f1;
th {
background: #fafafa;
padding: .5rem;
}
td {
padding: .5rem;
.ant-tag {
font-size: .8rem !important;
}
}
}
}
.alert {
margin-bottom: .5rem;
}
}
</style>

View File

@ -0,0 +1,190 @@
<template>
<div style="width: 100%">
<div class="option-area">
<a-upload
class="upload-area"
:fileList="fileList"
:remove="handleRemove"
:disabled="fileList.length === 1"
:beforeUpload="beforeUpload">
<a-button>
<a-icon type="upload" /> 选择.xlsx文件
</a-button>
</a-upload>
<div class="button-area">
<a-button type="primary" @click="downloadTemplate" style="margin-right: .5rem">
模板下载
</a-button>
<a-button @click="exportExcel" style="margin-right: .5rem">
导出Excel
</a-button>
<a-button
@click="handleUpload"
:disabled="fileList.length === 0"
:loading="uploading">
{{uploading ? '导入中' : '导入Excel' }}
</a-button>
</div>
</div>
<a-card :bordered="false" class="card-area">
<!-- 表格区域 -->
<a-table ref="TableInfo"
:columns="columns"
:dataSource="dataSource"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
:scroll="{ x: 900 }">
</a-table>
</a-card>
<import-result
@close="handleClose"
:importData="importData"
:errors="errors"
:times="times"
:importResultVisible="importResultVisible">
</import-result>
</div>
</template>
<script>
import ImportResult from './ImportResult'
export default {
components: {ImportResult},
data () {
return {
fileList: [],
importData: [],
times: '',
errors: [],
uploading: false,
importResultVisible: false,
dataSource: [],
paginationInfo: null,
pagination: {
pageSizeOptions: ['10', '20', '30', '40', '100'],
defaultCurrent: 1,
defaultPageSize: 10,
showQuickJumper: true,
showSizeChanger: true,
showTotal: (total, range) => `显示 ${range[0]} ~ ${range[1]} 条记录,共 ${total} 条记录`
},
loading: false
}
},
computed: {
columns () {
return [{
title: '字段1',
dataIndex: 'field1'
}, {
title: '字段2',
dataIndex: 'field2'
}, {
title: '字段3',
dataIndex: 'field3'
}, {
title: '导入时间',
dataIndex: 'createTime'
}]
}
},
mounted () {
this.fetch()
},
methods: {
handleClose () {
this.importResultVisible = false
},
downloadTemplate () {
this.$download('test/template', {}, '导入模板.xlsx')
},
exportExcel () {
this.$export('test/export')
},
handleRemove (file) {
if (this.uploading) {
this.$message.warning('文件导入中,请勿删除')
return
}
const index = this.fileList.indexOf(file)
const newFileList = this.fileList.slice()
newFileList.splice(index, 1)
this.fileList = newFileList
},
beforeUpload (file) {
this.fileList = [...this.fileList, file]
return false
},
handleUpload () {
const { fileList } = this
const formData = new FormData()
formData.append('file', fileList[0])
this.uploading = true
this.$upload('test/import', formData).then((r) => {
let data = r.data.data
if (data.data.length) {
this.fetch()
}
this.importData = data.data
this.errors = data.error
this.times = data.time / 1000
this.uploading = false
this.fileList = []
this.importResultVisible = true
}).catch((r) => {
console.error(r)
this.uploading = false
this.fileList = []
})
},
handleTableChange (pagination, filters, sorter) {
this.paginationInfo = pagination
this.fetch()
},
fetch (params = {}) {
this.loading = true
if (this.paginationInfo) {
this.$refs.TableInfo.pagination.current = this.paginationInfo.current
this.$refs.TableInfo.pagination.pageSize = this.paginationInfo.pageSize
params.pageSize = this.paginationInfo.pageSize
params.pageNum = this.paginationInfo.current
} else {
params.pageSize = this.pagination.defaultPageSize
params.pageNum = this.pagination.defaultCurrent
}
this.$get('test', {
...params
}).then((r) => {
let data = r.data
const pagination = { ...this.pagination }
pagination.total = data.total
this.loading = false
this.dataSource = data.rows
this.pagination = pagination
})
}
}
}
</script>
<style lang="less" scoped>
@import "../../../static/less/Common";
.option-area {
display: inline-block;
width: 100%;
padding: 0 .9rem;
margin: .5rem 0;
.upload-area {
display: inline;
float: left;
width: 50%
}
.button-area {
margin-left: 1rem;
display: inline;
float: right;
width: 30%;
}
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<a-modal
class="import-result"
title="导入结果"
v-model="show"
:centered="true"
:footer="null"
:maskClosable="false"
:width=1000
@cancel="handleCancel">
<div class="import-desc">
<span v-if="importData.length === 0 && errors.length === 0">
<a-alert
message="暂无导入记录"
type="info">
</a-alert>
</span>
<span v-if="importData.length !== 0 && errors.length !== 0">
<a-alert
message="部分导入成功"
type="warning">
<div slot="description">
成功导入 <a>{{importData.length}}</a> 条记录<a>{{errors.length}}</a> 条记录导入失败共耗时 <a>{{times}}</a>
</div>
</a-alert>
</span>
<span v-if="importData.length !== 0 && errors.length === 0">
<a-alert
message="全部导入成功"
type="success">
<div slot="description">
成功导入 <a>{{importData.length}}</a> 条记录共耗时 <a>{{times}}</a>
</div>
</a-alert>
</span>
<span v-if="importData.length === 0 && errors.length !== 0">
<a-alert
message="全部导入失败"
type="error">
<div slot="description">
<a>{{errors.length}}</a> 条记录导入失败共耗时 <a>{{times}}</a>
</div>
</a-alert>
</span>
</div>
<a-tabs defaultActiveKey="1">
<a-tab-pane tab="成功记录" key="1" v-if="importData.length">
<a-table ref="successTable"
:columns="successColumns"
:dataSource="importData"
:pagination="pagination"
:scroll="{ x: 900 }">
</a-table>
</a-tab-pane>
<a-tab-pane tab="失败记录" key="2" v-if="errors.length">
<a-table ref="errorTable"
:columns="errorColumns"
:dataSource="errorsData"
:pagination="pagination"
:scroll="{ x: 900 }">
</a-table>
</a-tab-pane>
</a-tabs>
</a-modal>
</template>
<script>
export default {
props: {
importResultVisible: {
required: true,
default: false
},
importData: {
required: true
},
errors: {
required: true
},
times: {
required: true
}
},
data () {
return {
pagination: {
pageSizeOptions: ['5', '10'],
defaultCurrent: 1,
defaultPageSize: 5,
showQuickJumper: true,
showSizeChanger: true,
showTotal: (total, range) => `显示 ${range[0]} ~ ${range[1]} 条记录,共 ${total} 条记录`
}
}
},
computed: {
errorsData () {
let arr = []
for (let i = 0; i < this.errors.length; i++) {
let error = this.errors[i]
let e = {}
for (let field of error.errorFields) {
e = {...field}
e.row = error.row
arr.push(e)
}
}
return arr
},
successColumns () {
return [{
title: '字段1',
dataIndex: 'field1'
}, {
title: '字段2',
dataIndex: 'field2'
}, {
title: '字段3',
dataIndex: 'field3'
}, {
title: '导入时间',
dataIndex: 'createTime'
}]
},
errorColumns () {
return [{
title: '行',
dataIndex: 'row',
customRender: (text, row, index) => {
return `${text + 1}`
}
}, {
title: '列',
dataIndex: 'cellIndex',
customRender: (text, row, index) => {
return `${text + 1}`
}
}, {
title: '列名',
dataIndex: 'column'
}, {
title: '错误信息',
dataIndex: 'errorMessage'
}]
},
show: {
get: function () {
return this.importResultVisible
},
set: function () {
}
}
},
methods: {
handleCancel () {
this.$emit('close')
}
}
}
</script>
<style lang="less">
.import-result {
.import-desc {
margin-bottom: .5rem;
a {
font-weight: 600;
}
}
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<div :class="[multipage === true ? 'multi-page':'single-page', 'not-menu-page', 'user-profile']">
<a-card title="">
<a href="javascript:void(0)" slot="extra" @click="updateProfile">编辑资料</a>
<a-row :gutter="8">
<a-col :span="6">
<a-row style="text-align: center">
<img style="width: 10rem;border-radius: 2px" alt="头像" :src="avatar">
</a-row>
<a-row style="text-align: center">
<a-button icon="edit" style="margin-top:1rem" @click="updateAvatar">修改头像</a-button>
</a-row>
</a-col>
<a-col :span="12" style="font-size: 1rem">
<p>账户{{user.username}}</p>
<p :title="user.roleName">角色{{user.roleName? user.roleName: '暂无角色'}}</p>
<p>性别{{sex}}</p>
<p>电话{{user.mobile ? user.mobile : '暂未绑定电话'}}</p>
<p>邮箱{{user.email ? user.email : '暂未绑定邮箱'}}</p>
<p>部门{{user.deptName? user.deptName: '暂无部门'}}</p>
<p>描述{{user.description}}</p>
</a-col>
</a-row>
</a-card>
<update-avatar
@cancel="handleUpdateAvatarCancel"
@success="handleUpdateAvatarSuccess"
:user="user"
:updateAvatarModelVisible="updateAvatarModelVisible">
</update-avatar>
<update-profile
ref="updateProfile"
@success="handleProfileEditSuccess"
@close="handleProfileEditClose"
:profileEditVisiable="profileEditVisiable">
</update-profile>
</div>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
import UpdateAvatar from './UpdateAvatar'
import UpdateProfile from './UpdateProfile'
export default {
name: 'Profile',
components: {UpdateAvatar, UpdateProfile},
data () {
return {
updateAvatarModelVisible: false,
profileEditVisiable: false,
userId: ''
}
},
computed: {
...mapState({
multipage: state => state.setting.multipage,
user: state => state.account.user
}),
avatar () {
return `static/avatar/${this.user.avatar}`
},
sex () {
switch (this.user.ssex) {
case '0':
return '男'
case '1':
return '女'
case '2':
return '保密'
default:
return this.user.ssex
}
}
},
methods: {
...mapMutations({
setUser: 'account/setUser'
}),
handleUpdateAvatarCancel () {
this.updateAvatarModelVisible = false
},
handleUpdateAvatarSuccess (avatar) {
this.updateAvatarModelVisible = false
this.$message.success('更换头像成功')
let user = this.user
user.avatar = avatar
this.setUser(user)
},
updateAvatar () {
this.updateAvatarModelVisible = true
this.userId = this.user.userId
},
updateProfile () {
this.$refs.updateProfile.setFormValues(this.user)
this.profileEditVisiable = true
},
handleProfileEditClose () {
this.profileEditVisiable = false
},
handleProfileEditSuccess () {
this.profileEditVisiable = false
this.$message.success('修改成功')
}
}
}
</script>
<style lang="less">
.user-profile {
.ant-card-body {
padding: 1rem 0 !important;
p {
font-size: .9rem !important;
margin-bottom: .6rem !important;
}
}
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<a-modal
class="update-avatar"
title="选择头像"
@cancel="handleCancel"
:width="710"
:footer="null"
v-model="show">
<a-tabs defaultActiveKey="1" class="avatar-tabs">
<a-tab-pane tab="后田花子" key="1">
<template v-for="(avatar, index) in hthz">
<div class="avatar-wrapper" :key="index">
<img alt="点击选择" :src="'static/avatar/' + avatar" @click="change(avatar)">
</div>
</template>
</a-tab-pane>
<a-tab-pane tab="阿里系" key="2" forceRender>
<template v-for="(avatar, index) in al">
<div class="avatar-wrapper" :key="index">
<img alt="点击选择" :src="'static/avatar/' + avatar" @click="change(avatar)">
</div>
</template>
</a-tab-pane>
<a-tab-pane tab="脸萌" key="3">
<template v-for="(avatar, index) in lm">
<div class="avatar-wrapper" :key="index">
<img alt="点击选择" :src="'static/avatar/' + avatar" @click="change(avatar)">
</div>
</template>
</a-tab-pane>
</a-tabs>
</a-modal>
</template>
<script>
const hthz = ['default.jpg', '1d22f3e41d284f50b2c8fc32e0788698.jpeg',
'2dd7a2d09fa94bf8b5c52e5318868b4d9.jpg', '2dd7a2d09fa94bf8b5c52e5318868b4df.jpg',
'8f5b60ef00714a399ee544d331231820.jpeg', '17e420c250804efe904a09a33796d5a10.jpg',
'17e420c250804efe904a09a33796d5a16.jpg', '87d8194bc9834e9f8f0228e9e530beb1.jpeg',
'496b3ace787342f7954b7045b8b06804.jpeg', '595ba7b05f2e485eb50565a50cb6cc3c.jpeg',
'964e40b005724165b8cf772355796c8c.jpeg', '5997fedcc7bd4cffbd350b40d1b5b987.jpg',
'5997fedcc7bd4cffbd350b40d1b5b9824.jpg', 'a3b10296862e40edb811418d64455d00.jpeg',
'a43456282d684e0b9319cf332f8ac468.jpeg', 'bba284ac05b041a8b8b0d1927868d5c9x.jpg',
'c7c4ee7be3eb4e73a19887dc713505145.jpg', 'ff698bb2d25c4d218b3256b46c706ece.jpeg']
const al = ['cnrhVkzwxjPwAaCfPbdc.png', 'BiazfanxmamNRoxxVxka.png', 'gaOngJwsRYRaVAuXXcmB.png',
'WhxKECPNujWoWEFNdnJE.png', 'ubnKSIfAJTxIgXOKlciN.png', 'jZUIxmJycoymBprLOUbT.png']
const lm = ['19034103295190235.jpg', '20180414165920.jpg', '20180414170003.jpg',
'20180414165927.jpg', '20180414165754.jpg', '20180414165815.jpg',
'20180414165821.jpg', '20180414165827.jpg', '20180414165834.jpg',
'20180414165840.jpg', '20180414165846.jpg', '20180414165855.jpg',
'20180414165909.jpg', '20180414165914.jpg', '20180414165936.jpg',
'20180414165942.jpg', '20180414165947.jpg', '20180414165955.jpg']
export default {
props: {
updateAvatarModelVisible: {
default: false
},
user: {
required: true
}
},
data () {
return {
hthz,
al,
lm,
updating: false
}
},
computed: {
show: {
get: function () {
return this.updateAvatarModelVisible
},
set: function () {
}
}
},
methods: {
handleCancel () {
this.$emit('cancel')
},
change (avatar) {
if (this.updating) {
this.$message.warning('更换头像中,请勿重复点击')
return
}
this.updating = true
this.$put('user/avatar', {
username: this.user.username,
avatar
}).then(() => {
this.$emit('success', avatar)
this.updating = false
}).catch((r) => {
console.error(r)
this.$message.error('更新头像失败')
this.updating = false
})
}
}
}
</script>
<style lang="less">
.update-avatar {
.ant-modal-body {
padding: 0 1rem 1rem 1rem!important;
.avatar-tabs {
.avatar-wrapper {
display: inline-block;
img {
width: 6rem;
border-radius: 2px;
display: inline-block;
margin: .5rem;
cursor: pointer;
}
}
}
}
}
</style>

View File

@ -0,0 +1,223 @@
<template>
<div>
<!-- 密码修改 -->
<a-modal
title="密码修改"
:keyboard="false"
:maskClosable="false"
:closable="false"
v-model="show"
@cancel="cancelUpdatePassword"
@ok="handleUpdatePassword">
<a-form :autoFormCreate="(form)=>{this.form = form}">
<a-form-item
label='旧密码'
v-bind="formItemLayout"
fieldDecoratorId="oldPassword"
:fieldDecoratorOptions="{rules: [{ required: true, message: '请输入旧密码'}, { validator: this.handleOldPassowrd }], validateTrigger: ['blur']}">
<a-input type="password"
autocomplete="false"
v-model="oldPassword"
placeholder="请输入旧密码"></a-input>
</a-form-item>
<a-popover placement="rightTop" trigger="click" :visible="state.passwordLevelChecked">
<template slot="content">
<div :style="{ width: '240px' }">
<div :class="['update-password', passwordLevelClass]">强度<span>{{ passwordLevelName }}</span></div>
<a-progress :percent="state.percent" :showInfo="false" :strokeColor=" passwordLevelColor "/>
<div style="margin-top: 10px;">
<span>请至少输入 6 个字符请不要使用容易被猜到的密码</span>
</div>
</div>
</template>
<a-form-item
label='新密码'
v-bind="formItemLayout"
fieldDecoratorId="password"
:fieldDecoratorOptions="{rules: [{ required: true, message: '至少6位密码区分大小写'}, { validator: this.handlePasswordLevel }], validateTrigger: ['change', 'blur']}">
<a-input type="password"
@click="handlePasswordInputClick"
v-model="newPassword"
autocomplete="false"
placeholder="至少6位密码区分大小写"></a-input>
</a-form-item>
</a-popover>
<a-form-item
label='再次确认'
v-bind="formItemLayout"
fieldDecoratorId="password2"
:fieldDecoratorOptions="{rules: [{ required: true, message: '至少6位密码区分大小写' }, { validator: this.handlePasswordCheck }], validateTrigger: ['change', 'blur']}">
<a-input type="password" autocomplete="false" placeholder="确认密码"></a-input>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
const formItemLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 18 }
}
const levelNames = {
0: '低',
1: '低',
2: '中',
3: '强'
}
const levelClass = {
0: 'error',
1: 'error',
2: 'warning',
3: 'success'
}
const levelColor = {
0: '#ff0000',
1: '#ff0000',
2: '#ff7e05',
3: '#52c41a'
}
export default {
props: {
updatePasswordModelVisible: {
default: false
},
user: {
required: true
}
},
data () {
return {
form: null,
formItemLayout,
state: {
passwordLevel: 0,
passwordLevelChecked: false,
percent: 10,
progressColor: '#FF0000'
},
oldPassword: '',
newPassword: '',
validateStatus: '',
help: ''
}
},
computed: {
show: {
get: function () {
return this.updatePasswordModelVisible
},
set: function () {
}
},
passwordLevelClass () {
return levelClass[this.state.passwordLevel]
},
passwordLevelName () {
return levelNames[this.state.passwordLevel]
},
passwordLevelColor () {
return levelColor[this.state.passwordLevel]
}
},
methods: {
isMobile () {
return this.$store.state.setting.isMobile
},
cancelUpdatePassword () {
this.state.passwordLevelChecked = false
this.form.resetFields()
this.$emit('cancel')
},
handleUpdatePassword () {
this.form.validateFields((err, values) => {
if (!err) {
this.$put('user/password', {
password: this.newPassword,
username: this.user.username
}).then(() => {
this.state.passwordLevelChecked = false
this.$emit('success')
this.form.resetFields()
})
}
})
},
handlePasswordLevel (rule, value, callback) {
let level = 0
//
if (/[0-9]/.test(value)) {
level++
}
//
if (/[a-zA-Z]/.test(value)) {
level++
}
//
if (/[^0-9a-zA-Z_]/.test(value)) {
level++
}
this.state.passwordLevel = level
this.state.percent = level * 30
if (level >= 2) {
if (level >= 3) {
this.state.percent = 100
}
callback()
} else {
if (level === 0) {
this.state.percent = 10
}
callback(new Error('密码强度不够'))
}
},
handlePasswordCheck (rule, value, callback) {
let password = this.form.getFieldValue('password')
if (value === undefined) {
callback(new Error('请输入密码'))
}
if (value && password && value.trim() !== password.trim()) {
callback(new Error('两次密码不一致'))
}
callback()
},
handlePasswordInputClick () {
if (!this.isMobile()) {
this.state.passwordLevelChecked = true
return
}
this.state.passwordLevelChecked = false
},
handleOldPassowrd (rule, value, callback) {
let password = this.oldPassword
if (this.oldPassword.trim().length) {
this.$get('user/password/check', {
password: password,
username: this.user.username
}).then((r) => {
if (r.data) {
callback()
} else {
callback(new Error('旧密码不正确'))
}
})
} else {
callback()
}
}
}
}
</script>
<style lang="less">
.update-password {
&.error {
color: #ff0000;
}
&.warning {
color: #ff7e05;
}
&.success {
color: #52c41a;
}
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<a-drawer
title="编辑资料"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="profileEditVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='邮箱' v-bind="formItemLayout">
<a-input
v-decorator="[
'email',
{rules: [
{ type: 'email', message: '请输入正确的邮箱' },
{ max: 50, message: '长度不能超过50个字符'}
]}
]"/>
</a-form-item>
<a-form-item label="手机" v-bind="formItemLayout">
<a-input
v-decorator="['mobile', {rules: [
{ pattern: '^0?(13[0-9]|15[012356789]|17[013678]|18[0-9]|14[57])[0-9]{8}$', message: '请输入正确的手机号'}
]}]"/>
</a-form-item>
<a-form-item label='部门' v-bind="formItemLayout">
<a-tree-select
:allowClear="true"
:dropdownStyle="{ maxHeight: '220px', overflow: 'auto' }"
:treeData="deptTreeData"
@change="onDeptChange"
:value="userDept">
</a-tree-select>
</a-form-item>
<a-form-item label='性别' v-bind="formItemLayout">
<a-radio-group
v-decorator="[
'ssex',
{rules: [{ required: true, message: '请选择性别' }]}
]">
<a-radio value="0"></a-radio>
<a-radio value="1"></a-radio>
<a-radio value="2">保密</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label='描述' v-bind="formItemLayout">
<a-textarea
:rows="4"
v-decorator="[
'description',
{rules: [
{ max: 100, message: '长度不能超过100个字符'}
]}]">
</a-textarea>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
</a-drawer>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
props: {
profileEditVisiable: {
default: false
}
},
data () {
return {
formItemLayout,
form: this.$form.createForm(this),
deptTreeData: [],
userDept: [],
userId: '',
roleId: '',
status: '',
username: '',
loading: false
}
},
computed: {
...mapState({
currentUser: state => state.account.user
})
},
methods: {
...mapMutations({
setUser: 'account/setUser'
}),
onClose () {
this.loading = false
this.form.resetFields()
this.$emit('close')
},
setFormValues ({...user}) {
this.userId = user.userId
let fields = ['email', 'ssex', 'description', 'mobile']
Object.keys(user).forEach((key) => {
if (fields.indexOf(key) !== -1) {
this.form.getFieldDecorator(key)
let obj = {}
obj[key] = user[key]
this.form.setFieldsValue(obj)
}
})
if (user.deptId) {
this.userDept = [user.deptId]
}
this.status = user.status
this.roleId = user.roleId
this.username = user.username
},
onDeptChange (value) {
this.userDept = value
},
handleSubmit () {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
let user = this.form.getFieldsValue()
user.userId = this.userId
user.deptId = this.userDept
user.roleId = this.roleId
user.status = this.status
user.username = this.username
this.$put('user/profile', {
...user
}).then((r) => {
this.loading = false
this.$emit('success')
// state
this.$get(`user/${user.username}`).then((r) => {
this.setUser(r.data)
})
}).catch(() => {
this.loading = false
})
}
})
}
},
watch: {
profileEditVisiable () {
if (this.profileEditVisiable) {
this.$get('dept').then((r) => {
this.deptTreeData = r.data.rows.children
})
}
}
}
}
</script>

View File

@ -0,0 +1,251 @@
<template>
<a-card :bordered="false" class="card-area">
<div :class="advanced ? 'search' : null">
<!-- 搜索区域 -->
<a-form layout="horizontal">
<div :class="advanced ? null: 'fold'">
<a-row >
<a-col :md="12" :sm="24" >
<a-form-item
label="名称"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<a-input v-model="queryParams.deptName"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" >
<a-form-item
label="创建时间"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<range-date @change="handleDateChange" ref="createTime"></range-date>
</a-form-item>
</a-col>
</a-row>
</div>
<span style="float: right; margin-top: 3px;">
<a-button type="primary" @click="search">查询</a-button>
<a-button style="margin-left: 8px" @click="reset">重置</a-button>
</span>
</a-form>
</div>
<div>
<div class="operator">
<a-button v-hasPermission="'dept:add'" type="primary" ghost @click="add">新增</a-button>
<a-button v-hasPermission="'dept:delete'" @click="batchDelete">删除</a-button>
<a-dropdown v-hasPermission="'dept:export'">
<a-menu slot="overlay">
<a-menu-item key="export-data" @click="exportExcel">导出Excel</a-menu-item>
</a-menu>
<a-button>
更多操作 <a-icon type="down" />
</a-button>
</a-dropdown>
</div>
<!-- 表格区域 -->
<a-table :columns="columns"
:dataSource="dataSource"
:pagination="pagination"
:loading="loading"
:scroll="{ x: 900 }"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
@change="handleTableChange">
<template slot="operation" slot-scope="text, record">
<a-icon v-hasPermission="'dept:update'" type="setting" theme="twoTone" twoToneColor="#4a9ff5" @click="edit(record)" title="修改"></a-icon>
<a-badge v-hasNoPermission="'dept:update'" status="warning" text="无权限"></a-badge>
</template>
</a-table>
</div>
<!-- 新增部门 -->
<dept-add
@success="handleDeptAddSuccess"
@close="handleDeptAddClose"
:deptAddVisiable="deptAddVisiable">
</dept-add>
<!-- 修改部门 -->
<dept-edit
ref="deptEdit"
@success="handleDeptEditSuccess"
@close="handleDeptEditClose"
:deptEditVisiable="deptEditVisiable">
</dept-edit>
</a-card>
</template>
<script>
import RangeDate from '@/components/datetime/RangeDate'
import DeptAdd from './DeptAdd'
import DeptEdit from './DeptEdit'
export default {
name: 'Dept',
components: {DeptAdd, DeptEdit, RangeDate},
data () {
return {
advanced: false,
dataSource: [],
selectedRowKeys: [],
queryParams: {},
sortedInfo: null,
pagination: {
defaultPageSize: 10000000,
hideOnSinglePage: true,
indentSize: 100
},
loading: false,
deptAddVisiable: false,
deptEditVisiable: false
}
},
computed: {
columns () {
let { sortedInfo } = this
sortedInfo = sortedInfo || {}
return [{
title: '名称',
dataIndex: 'text'
}, {
title: '排序',
dataIndex: 'order'
}, {
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
sortOrder: sortedInfo.columnKey === 'createTime' && sortedInfo.order
}, {
title: '修改时间',
dataIndex: 'modifyTime',
sorter: true,
sortOrder: sortedInfo.columnKey === 'modifyTime' && sortedInfo.order
}, {
title: '操作',
dataIndex: 'operation',
scopedSlots: { customRender: 'operation' },
fixed: 'right',
width: 120
}]
}
},
mounted () {
this.fetch()
},
methods: {
onSelectChange (selectedRowKeys) {
this.selectedRowKeys = selectedRowKeys
},
handleDeptAddClose () {
this.deptAddVisiable = false
},
handleDeptAddSuccess () {
this.deptAddVisiable = false
this.$message.success('新增部门成功')
this.fetch()
},
add () {
this.deptAddVisiable = true
},
handleDeptEditClose () {
this.deptEditVisiable = false
},
handleDeptEditSuccess () {
this.deptEditVisiable = false
this.$message.success('修改部门成功')
this.fetch()
},
edit (record) {
this.deptEditVisiable = true
this.$refs.deptEdit.setFormValues(record)
},
handleDateChange (value) {
if (value) {
this.queryParams.createTimeFrom = value[0]
this.queryParams.createTimeTo = value[1]
}
},
batchDelete () {
if (!this.selectedRowKeys.length) {
this.$message.warning('请选择需要删除的记录')
return
}
let that = this
this.$confirm({
title: '确定删除所选中的记录?',
content: '当您点击确定按钮后,这些记录将会被彻底删除,如果其包含子记录,也将一并删除!',
centered: true,
onOk () {
that.$delete('dept/' + that.selectedRowKeys.join(',')).then(() => {
that.$message.success('删除成功')
that.selectedRowKeys = []
that.fetch()
})
},
onCancel () {
that.selectedRowKeys = []
}
})
},
exportExcel () {
let {sortedInfo} = this
let sortField, sortOrder
//
if (sortedInfo) {
sortField = sortedInfo.field
sortOrder = sortedInfo.order
}
this.$export('dept/excel', {
sortField: sortField,
sortOrder: sortOrder,
...this.queryParams
})
},
search () {
let {sortedInfo} = this
let sortField, sortOrder
//
if (sortedInfo) {
sortField = sortedInfo.field
sortOrder = sortedInfo.order
}
this.fetch({
sortField: sortField,
sortOrder: sortOrder,
...this.queryParams
})
},
reset () {
//
this.selectedRowKeys = []
//
this.sortedInfo = null
//
this.queryParams = {}
//
this.$refs.createTime.reset()
this.fetch()
},
handleTableChange (pagination, filters, sorter) {
this.sortedInfo = sorter
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
...this.queryParams,
...filters
})
},
fetch (params = {}) {
this.loading = true
this.$get('dept', {
...params
}).then((r) => {
let data = r.data
this.loading = false
this.dataSource = data.rows.children
})
}
}
}
</script>
<style lang="less" scoped>
@import "../../../../static/less/Common";
</style>

View File

@ -0,0 +1,123 @@
<template>
<a-drawer
title="新增部门"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="deptAddVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='部门名称' v-bind="formItemLayout">
<a-input v-model="dept.deptName"
v-decorator="['deptName',
{rules: [
{ required: true, message: '部门名称不能为空'},
{ max: 20, message: '长度不能超过20个字符'}
]}]"/>
</a-form-item>
<a-form-item label='部门排序' v-bind="formItemLayout">
<a-input-number v-model="dept.orderNum" style="width: 100%"/>
</a-form-item>
<a-form-item label='上级部门'
style="margin-bottom: 2rem"
v-bind="formItemLayout">
<a-tree
:key="deptTreeKeys"
:checkable="true"
:checkStrictly="true"
@check="handleCheck"
@expand="handleExpand"
:expandedKeys="expandedKeys"
:treeData="deptTreeData">
</a-tree>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
</a-drawer>
</template>
<script>
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
name: 'DeptAdd',
props: {
deptAddVisiable: {
default: false
}
},
data () {
return {
loading: false,
formItemLayout,
form: this.$form.createForm(this),
dept: {},
checkedKeys: [],
expandedKeys: [],
deptTreeData: [],
deptTreeKeys: +new Date()
}
},
methods: {
reset () {
this.loading = false
this.deptTreeKeys = +new Date()
this.expandedKeys = this.checkedKeys = []
this.dept = {}
this.form.resetFields()
},
onClose () {
this.reset()
this.$emit('close')
},
handleCheck (checkedKeys) {
this.checkedKeys = checkedKeys
},
handleExpand (expandedKeys) {
this.expandedKeys = expandedKeys
},
handleSubmit () {
let checkedArr = Object.is(this.checkedKeys.checked, undefined) ? this.checkedKeys : this.checkedKeys.checked
if (checkedArr.length > 1) {
this.$message.error('最多只能选择一个上级部门,请修改')
return
}
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
if (checkedArr.length) {
this.dept.parentId = checkedArr[0]
} else {
this.dept.parentId = ''
}
this.$post('dept', {
...this.dept
}).then(() => {
this.reset()
this.$emit('success')
}).catch(() => {
this.loading = false
})
}
})
}
},
watch: {
deptAddVisiable () {
if (this.deptAddVisiable) {
this.$get('dept').then((r) => {
this.deptTreeData = r.data.rows.children
})
}
}
}
}
</script>

View File

@ -0,0 +1,142 @@
<template>
<a-drawer
title="修改按钮"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="deptEditVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='部门名称' v-bind="formItemLayout">
<a-input v-decorator="['deptName',
{rules: [
{ required: true, message: '部门名称不能为空'},
{ max: 20, message: '长度不能超过20个字符'}
]}]"/>
</a-form-item>
<a-form-item label='部门排序' v-bind="formItemLayout">
<a-input-number v-decorator="['orderNum']" style="width: 100%"/>
</a-form-item>
<a-form-item label='上级部门'
style="margin-bottom: 2rem"
v-bind="formItemLayout">
<a-tree
:key="deptTreeKey"
:checkable="true"
:checkStrictly="true"
@check="handleCheck"
@expand="handleExpand"
:expandedKeys="expandedKeys"
:defaultCheckedKeys="defaultCheckedKeys"
:treeData="deptTreeData">
</a-tree>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
</a-drawer>
</template>
<script>
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
name: 'DeptEdit',
props: {
deptEditVisiable: {
default: false
}
},
data () {
return {
loading: false,
formItemLayout,
form: this.$form.createForm(this),
deptTreeKey: +new Date(),
dept: {},
checkedKeys: [],
expandedKeys: [],
defaultCheckedKeys: [],
deptTreeData: []
}
},
methods: {
reset () {
this.loading = false
this.deptTreeKey = +new Date()
this.expandedKeys = this.checkedKeys = this.defaultCheckedKeys = []
this.button = {}
this.form.resetFields()
},
onClose () {
this.reset()
this.$emit('close')
},
handleCheck (checkedKeys) {
this.checkedKeys = checkedKeys
},
handleExpand (expandedKeys) {
this.expandedKeys = expandedKeys
},
setFormValues ({...dept}) {
this.form.getFieldDecorator('deptName')
this.form.setFieldsValue({'deptName': dept.text})
this.form.getFieldDecorator('orderNum')
this.form.setFieldsValue({'orderNum': dept.order})
if (dept.parentId !== '0') {
this.defaultCheckedKeys.push(dept.parentId)
this.checkedKeys = this.defaultCheckedKeys
this.expandedKeys = this.checkedKeys
}
this.dept.deptId = dept.id
},
handleSubmit () {
let checkedArr = Object.is(this.checkedKeys.checked, undefined) ? this.checkedKeys : this.checkedKeys.checked
if (checkedArr.length > 1) {
this.$message.error('最多只能选择一个上级部门,请修改')
return
}
if (checkedArr[0] === this.dept.deptId) {
this.$message.error('不能选择自己作为上级部门,请修改')
return
}
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
let dept = this.form.getFieldsValue()
dept.parentId = checkedArr[0]
if (Object.is(dept.parentId, undefined)) {
dept.parentId = 0
}
dept.deptId = this.dept.deptId
this.$put('dept', {
...dept
}).then(() => {
this.reset()
this.$emit('success')
}).catch(() => {
this.loading = false
})
}
})
}
},
watch: {
deptEditVisiable () {
if (this.deptEditVisiable) {
this.$get('dept').then((r) => {
this.deptTreeData = r.data.rows.children
this.deptTreeKey = +new Date()
})
}
}
}
}
</script>

View File

@ -0,0 +1,35 @@
<template>
<a-tree-select
:allowClear="true"
:dropdownStyle="{ maxHeight: '220px', overflow: 'auto' }"
:treeData="deptTreeData"
v-model="value">
</a-tree-select>
</template>
<script>
export default {
name: 'DetpInputTree',
data () {
return {
deptTreeData: [],
value: undefined
}
},
methods: {
reset () {
this.value = ''
}
},
mounted () {
this.$get('dept').then((r) => {
this.deptTreeData = r.data.rows.children
})
},
watch: {
value (value) {
this.$emit('change', value)
}
}
}
</script>

View File

@ -0,0 +1,272 @@
<template>
<a-card :bordered="false" class="card-area">
<div :class="advanced ? 'search' : null">
<!-- 搜索区域 -->
<a-form layout="horizontal">
<div :class="advanced ? null: 'fold'">
<a-row >
<a-col :md="12" :sm="24" >
<a-form-item
label="名称"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<a-input v-model="queryParams.name"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" >
<a-form-item
label="创建时间"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<range-date @change="handleDateChange" ref="createTime"></range-date>
</a-form-item>
</a-col>
</a-row>
</div>
<span style="float: right; margin-top: 3px;">
<a-button type="primary" @click="search">查询</a-button>
<a-button style="margin-left: 8px" @click="reset">重置</a-button>
</span>
</a-form>
</div>
<div>
<div class="operator">
<a-button v-hasPermission="'domain:add'" type="primary" ghost @click="add">新增</a-button>
<a-button v-hasPermission="'domain:delete'" @click="batchDelete">删除</a-button>
<a-dropdown v-hasPermission="'domain:export'">
<a-menu slot="overlay">
<a-menu-item key="export-data" @click="exportExcel">导出Excel</a-menu-item>
</a-menu>
<a-button>
更多操作 <a-icon type="down" />
</a-button>
</a-dropdown>
</div>
<!-- 表格区域 -->
<a-table :columns="columns"
:dataSource="dataSource"
:pagination="pagination"
:loading="loading"
:scroll="{ x: 900 }"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
@change="handleTableChange">
<template slot="operation" slot-scope="text, record">
<a-icon v-hasPermission="'domain:update'" type="setting" theme="twoTone" twoToneColor="#4a9ff5" @click="edit(record)" title="修改"></a-icon>
<a-icon type="eye" theme="twoTone" twoToneColor="#42b983" @click="view(record)" title="查看"></a-icon>
</template>
</a-table>
</div>
<!-- 新增域 -->
<domain-add
@success="handleDomainAddSuccess"
@close="handleDomainAddClose"
:domainAddVisiable="domainAdd.visible">
</domain-add>
<!-- 修改域 -->
<domain-edit
ref="domainEdit"
@success="handleDomainEditSuccess"
@close="handleDomainEditClose"
:domainEditVisible="domainEdit.visible"
:domainEditData="domainEdit.data">
</domain-edit>
<domain-info
ref="domainInfo"
@close="handleDomainInfoClose"
:domainInfoVisible="domainInfo.visible"
:domainInfoData="domainInfo.data">
</domain-info>
</a-card>
</template>
<script>
import RangeDate from '@/components/datetime/RangeDate'
import DomainAdd from './DomainAdd'
import DomainEdit from './DomainEdit'
import DomainInfo from './DomainInfo'
export default {
name: 'Domain',
components: {DomainAdd, DomainEdit, DomainInfo, RangeDate},
data () {
return {
advanced: false,
domainAdd: {
visible: false
},
domainEdit: {
visible: false,
data: {}
},
domainInfo: {
visible: false,
data: {}
},
dataSource: [],
selectedRowKeys: [],
queryParams: {},
sortedInfo: null,
pagination: {
defaultPageSize: 10000000,
hideOnSinglePage: true,
indentSize: 100
},
loading: false
}
},
computed: {
columns () {
let { sortedInfo } = this
sortedInfo = sortedInfo || {}
return [{
title: '名称',
dataIndex: 'name'
}, {
title: '编号',
dataIndex: 'order'
}, {
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
sortOrder: sortedInfo.columnKey === 'createTime' && sortedInfo.order
}, {
title: '修改时间',
dataIndex: 'modifyTime',
sorter: true,
sortOrder: sortedInfo.columnKey === 'modifyTime' && sortedInfo.order
}, {
title: '操作',
dataIndex: 'operation',
scopedSlots: { customRender: 'operation' },
fixed: 'right',
width: 120
}]
}
},
mounted () {
this.fetch()
},
methods: {
onSelectChange (selectedRowKeys) {
this.selectedRowKeys = selectedRowKeys
},
handleDomainAddClose () {
this.domainAdd.visible = false
},
handleDomainAddSuccess () {
this.domainAdd.visible = false
this.$message.success('新增域成功')
this.fetch()
},
add () {
this.domainAdd.visible = true
},
handleDomainEditClose () {
this.domainEdit.visible = false
},
handleDomainEditSuccess () {
this.domainEdit.visible = false
this.$message.success('修改域成功')
this.fetch()
},
handleDomainInfoClose () {
this.domainInfo.visible = false
},
view (record) {
this.domainInfo.data = record
this.domainInfo.visible = true
},
edit (record) {
this.domainEdit.visible = true
this.$refs.domainEdit.setFormValues(record)
},
handleDateChange (value) {
if (value) {
this.queryParams.createTimeFrom = value[0]
this.queryParams.createTimeTo = value[1]
}
},
batchDelete () {
if (!this.selectedRowKeys.length) {
this.$message.warning('请选择需要删除的记录')
return
}
let that = this
this.$confirm({
title: '确定删除所选中的记录?',
content: '当您点击确定按钮后,这些记录将会被彻底删除,如果其包含子记录,也将一并删除!',
centered: true,
onOk () {
that.$delete('domains/' + that.selectedRowKeys.join(',')).then(() => {
that.$message.success('删除成功')
that.selectedRowKeys = []
that.fetch()
})
},
onCancel () {
that.selectedRowKeys = []
}
})
},
exportExcel () {
let {sortedInfo} = this
let sortField, sortOrder
//
if (sortedInfo) {
sortField = sortedInfo.field
sortOrder = sortedInfo.order
}
this.$export('domains/excel', {
sortField: sortField,
sortOrder: sortOrder,
...this.queryParams
})
},
search () {
let {sortedInfo} = this
let sortField, sortOrder
//
if (sortedInfo) {
sortField = sortedInfo.field
sortOrder = sortedInfo.order
}
this.fetch({
sortField: sortField,
sortOrder: sortOrder,
...this.queryParams
})
},
reset () {
//
this.selectedRowKeys = []
//
this.sortedInfo = null
//
this.queryParams = {}
//
this.$refs.createTime.reset()
this.fetch()
},
handleTableChange (pagination, filters, sorter) {
this.sortedInfo = sorter
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
...this.queryParams,
...filters
})
},
fetch (params = {}) {
this.loading = true
this.$get('domains', {
...params
}).then((r) => {
})
}
}
}
</script>
<style lang="less" scoped>
@import "../../../../static/less/Common";
</style>

View File

@ -0,0 +1,99 @@
<template>
<a-drawer
title="新增域"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="domainAddVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='域名称' v-bind="formItemLayout">
<a-input v-model="domain.name"
v-decorator="['name',
{rules: [
{ required: true, message: '部门名称不能为空'},
{ max: 20, message: '长度不能超过20个字符'}
]}]"/>
</a-form-item>
<a-form-item label='域编号' v-bind="formItemLayout">
<a-input-number v-model="domain.domainid" style="width: 100%"/>
</a-form-item>
<a-form-item label='域描述' v-bind="formItemLayout">
<a-input v-model="domain.description" style="width: 100%"/>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
</a-drawer>
</template>
<script>
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
name: 'DomainAdd',
props: {
domainAddVisiable: {
default: false
}
},
data () {
return {
loading: false,
formItemLayout,
form: this.$form.createForm(this),
domain: {},
enable: true
}
},
methods: {
reset () {
this.loading = false
this.expandedKeys = this.checkedKeys = []
this.domain = {}
this.form.resetFields()
},
onClose () {
this.reset()
this.$emit('close')
},
handleSubmit () {
let checkedArr = Object.is(this.checkedKeys.checked, undefined) ? this.checkedKeys : this.checkedKeys.checked
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
if (checkedArr.length) {
this.dept.parentId = checkedArr[0]
} else {
this.dept.parentId = ''
}
this.$post('auth/v1/domains', {
...this.dept
}).then(() => {
this.reset()
this.$emit('success')
}).catch(() => {
this.loading = false
})
}
})
}
},
watch: {
domainAddVisiable () {
if (this.domainAddVisiable) {
this.$get('/auth/v1/domains').then((r) => {
})
}
}
}
}
</script>

View File

@ -0,0 +1,112 @@
<template>
<a-drawer
title="修改域信息"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="domainEditVisible"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='域名称' v-bind="formItemLayout">
<a-input v-decorator="['domainName',
{rules: [
{ required: true, message: '域名称不能为空'},
{ max: 20, message: '长度不能超过20个字符'}
]}]"/>
</a-form-item>
<a-form-item label='域编号' v-bind="formItemLayout">
<a-input-number v-decorator="['orderNum']" style="width: 100%"/>
</a-form-item>
<a-form-item label='域描述' v-bind="formItemLayout">
<a-input v-decorator="['domainId']" style="width: 100%"/>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
</a-drawer>
</template>
<script>
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
name: 'DomainEdit',
props: {
domainEditVisible: {
default: false
}
},
data () {
return {
loading: false,
formItemLayout,
form: this.$form.createForm(this),
domain: {}
}
},
methods: {
reset () {
this.loading = false
this.button = {}
this.form.resetFields()
},
onClose () {
this.reset()
this.$emit('close')
},
handleCheck (checkedKeys) {
this.checkedKeys = checkedKeys
},
handleExpand (expandedKeys) {
this.expandedKeys = expandedKeys
},
setFormValues ({...domain}) {
this.form.getFieldDecorator('domainName')
this.form.setFieldsValue({'domainName': domain.name})
this.form.getFieldDecorator('orderNum')
this.form.setFieldsValue({'orderNum': domain.order})
this.form.getFieldDecorator('domainId')
this.form.setFieldsValue({'domainId': domain.domainId})
this.domain.domainId = domain.domainId
},
handleSubmit () {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
let domain = this.form.getFieldsValue()
domain.domainId = this.domain.domainId
this.$put('domains', {
...domain
}).then(() => {
this.reset()
this.$emit('success')
}).catch(() => {
this.loading = false
})
}
})
}
},
watch: {
domainEditVisible () {
if (this.domainEditVisible) {
this.$get('menu').then((r) => {
this.allTreeKeys = r.data.ids
this.$get('role/menu/' + this.roleInfoData.roleId).then((r) => {
this.defaultCheckedKeys.splice(0, this.defaultCheckedKeys.length, r.data)
this.checkedKeys = r.data
this.expandedKeys = r.data
})
})
}
}
}
}
</script>

View File

@ -0,0 +1,44 @@
<template>
<a-drawer
title="域信息"
:maskClosable="false"
width=650
placement="right"
:closable="true"
@close="close"
:visible="domainInfoVisible"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<p><a-icon type="crown" />&nbsp;&nbsp;域名称{{domainInfoData.roleName}}</p>
<p :title="domainInfoData.remark"><a-icon type="book" />&nbsp;&nbsp;域描述{{domainInfoData.remark}}</p>
<p><a-icon type="clock-circle" />&nbsp;&nbsp;创建时间{{domainInfoData.createTime}}</p>
<p><a-icon type="clock-circle" />&nbsp;&nbsp;修改时间{{domainInfoData.modifyTime? domainInfoData.modifyTime : '暂未修改'}}</p>
</a-drawer>
</template>
<script>
export default {
name: 'DomainInfo',
props: {
domainInfoVisible: {
require: true,
default: false
},
domainInfoData: {
require: true
}
},
data () {
return {
key: +new Date(),
loading: true,
checkedKeys: [],
menuTreeData: []
}
},
methods: {
close () {
this.$emit('close')
this.checkedKeys = []
}
}
}
</script>

View File

@ -0,0 +1,151 @@
<template>
<a-drawer
title="新增按钮"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="buttonAddVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='按钮名称' v-bind="formItemLayout">
<a-input v-model="button.menuName"
v-decorator="['menuName',
{rules: [
{ required: true, message: '按钮名称不能为空'},
{ max: 10, message: '长度不能超过10个字符'}
]}]"/>
</a-form-item>
<a-form-item label='相关权限' v-bind="formItemLayout">
<a-input v-model="button.perms"
v-decorator="['perms',
{rules: [
{ max: 50, message: '长度不能超过50个字符'}
]}]"/>
</a-form-item>
<a-form-item label='上级菜单'
style="margin-bottom: 2rem"
v-bind="formItemLayout">
<a-tree
:key="menuTreeKey"
:checkable="true"
:checkStrictly="true"
@check="handleCheck"
@expand="handleExpand"
:expandedKeys="expandedKeys"
:treeData="menuTreeData">
</a-tree>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-dropdown style="float: left" :trigger="['click']" placement="topCenter">
<a-menu slot="overlay">
<a-menu-item key="1" @click="expandAll">展开所有</a-menu-item>
<a-menu-item key="2" @click="closeAll">合并所有</a-menu-item>
</a-menu>
<a-button>
树操作 <a-icon type="up" />
</a-button>
</a-dropdown>
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
</a-drawer>
</template>
<script>
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
name: 'ButtonAdd',
props: {
buttonAddVisiable: {
default: false
}
},
data () {
return {
loading: false,
formItemLayout,
form: this.$form.createForm(this),
menuTreeKey: +new Date(),
button: {},
checkedKeys: [],
expandedKeys: [],
menuTreeData: []
}
},
methods: {
reset () {
this.loading = false
this.menuTreeKey = +new Date()
this.expandedKeys = this.checkedKeys = []
this.button = {}
this.form.resetFields()
},
onClose () {
this.reset()
this.$emit('close')
},
handleCheck (checkedKeys) {
this.checkedKeys = checkedKeys
},
expandAll () {
this.expandedKeys = this.allTreeKeys
},
closeAll () {
this.expandedKeys = []
},
handleExpand (expandedKeys) {
this.expandedKeys = expandedKeys
},
handleSubmit () {
let checkedArr = Object.is(this.checkedKeys.checked, undefined) ? this.checkedKeys : this.checkedKeys.checked
if (!checkedArr.length) {
this.$message.error('请为按钮选择一个上级菜单')
return
}
if (checkedArr.length > 1) {
this.$message.error('最多只能选择一个上级菜单,请修改')
return
}
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
if (checkedArr.length) {
this.button.parentId = checkedArr[0]
} else {
this.button.parentId = ''
}
// 0 1
this.button.type = '1'
this.$post('menu', {
...this.button
}).then(() => {
this.reset()
this.$emit('success')
}).catch(() => {
this.loading = false
})
}
})
}
},
watch: {
buttonAddVisiable () {
if (this.buttonAddVisiable) {
this.$get('menu', {
type: '0'
}).then((r) => {
this.menuTreeData = r.data.rows.children
this.allTreeKeys = r.data.ids
})
}
}
}
}
</script>

View File

@ -0,0 +1,161 @@
<template>
<a-drawer
title="修改按钮"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="buttonEditVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='按钮名称' v-bind="formItemLayout">
<a-input v-decorator="['menuName',
{rules: [
{ required: true, message: '按钮名称不能为空'},
{ max: 10, message: '长度不能超过10个字符'}
]}]"/>
</a-form-item>
<a-form-item label='相关权限' v-bind="formItemLayout">
<a-input v-decorator="['perms',
{rules: [
{ max: 50, message: '长度不能超过50个字符'}
]}]"/>
</a-form-item>
<a-form-item label='上级菜单'
style="margin-bottom: 2rem"
v-bind="formItemLayout">
<a-tree
:key="menuTreeKey"
:checkable="true"
:checkStrictly="true"
@check="handleCheck"
@expand="handleExpand"
:expandedKeys="expandedKeys"
:defaultCheckedKeys="defaultCheckedKeys"
:treeData="menuTreeData">
</a-tree>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-dropdown style="float: left" :trigger="['click']" placement="topCenter">
<a-menu slot="overlay">
<a-menu-item key="1" @click="expandAll">展开所有</a-menu-item>
<a-menu-item key="2" @click="closeAll">合并所有</a-menu-item>
</a-menu>
<a-button>
树操作 <a-icon type="up" />
</a-button>
</a-dropdown>
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
</a-drawer>
</template>
<script>
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
name: 'ButtonEdit',
props: {
buttonEditVisiable: {
default: false
}
},
data () {
return {
loading: false,
formItemLayout,
form: this.$form.createForm(this),
menuTreeKey: +new Date(),
button: {},
checkedKeys: [],
expandedKeys: [],
defaultCheckedKeys: [],
menuTreeData: []
}
},
methods: {
reset () {
this.loading = false
this.menuTreeKey = +new Date()
this.expandedKeys = this.checkedKeys = this.defaultCheckedKeys = []
this.button = {}
this.form.resetFields()
},
onClose () {
this.reset()
this.$emit('close')
},
handleCheck (checkedKeys) {
this.checkedKeys = checkedKeys
},
expandAll () {
this.expandedKeys = this.allTreeKeys
},
closeAll () {
this.expandedKeys = []
},
handleExpand (expandedKeys) {
this.expandedKeys = expandedKeys
},
setFormValues ({...menu}) {
this.form.getFieldDecorator('menuName')
this.form.setFieldsValue({'menuName': menu.text})
this.form.getFieldDecorator('perms')
this.form.setFieldsValue({'perms': menu.permission})
this.defaultCheckedKeys.push(menu.parentId)
this.checkedKeys = this.defaultCheckedKeys
this.expandedKeys = this.checkedKeys
this.button.menuId = menu.id
},
handleSubmit () {
let checkedArr = Object.is(this.checkedKeys.checked, undefined) ? this.checkedKeys : this.checkedKeys.checked
if (!checkedArr.length) {
this.$message.error('请为按钮选择一个上级菜单')
return
}
if (checkedArr.length > 1) {
this.$message.error('最多只能选择一个上级菜单,请修改')
return
}
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
let button = this.form.getFieldsValue()
button.parentId = checkedArr[0]
// 0 1
button.type = '1'
button.menuId = this.button.menuId
this.$put('menu', {
...button
}).then(() => {
this.reset()
this.$emit('success')
}).catch(() => {
this.loading = false
})
}
})
}
},
watch: {
buttonEditVisiable () {
if (this.buttonEditVisiable) {
this.$get('menu', {
type: '0'
}).then((r) => {
this.menuTreeData = r.data.rows.children
this.allTreeKeys = r.data.ids
this.menuTreeKey = +new Date()
})
}
}
}
}
</script>

View File

@ -0,0 +1,35 @@
@active-color: #4a4a48;
ul {
max-height: 700px;
overflow-y: auto;
padding-left: .5rem;
i {
font-size: 1.5rem;
border: 1px solid #f1f1f1;
padding: .2rem;
margin: .3rem;
cursor: pointer;
&.active, &:hover {
border-radius: 2px;
border-color: @active-color;
background-color: @active-color;
color: #fff;
transition: all .3s;
}
}
li {
list-style: none;
float: left;
width: 5%;
text-align: center;
cursor: pointer;
color: #555;
transition: color .3s ease-in-out,background-color .3s ease-in-out;
position: relative;
margin: 3px 0;
border-radius: 4px;
background-color: #fff;
overflow: hidden;
padding: 10px 0 0;
}
}

View File

@ -0,0 +1,123 @@
<template>
<a-modal
v-model="show"
:width="900"
:keyboard="false"
:closable="false"
:centered="true"
@ok="ok"
@cancel="cancel"
:maskClosable="false"
:mask="false"
okText="确认"
cancelText="取消">
<a-tabs>
<a-tab-pane tab="方向性图标" key="1">
<ul>
<li v-for="icon in icons.directionIcons" :key="icon">
<a-icon :type="icon" :title="icon" @click="chooseIcon(icon)" :class="{'active':activeIndex === icon}"/>
</li>
</ul>
</a-tab-pane>
<a-tab-pane tab="指示性图标" key="2">
<ul>
<li v-for="icon in icons.suggestionIcons" :key="icon">
<a-icon :type="icon" :title="icon" @click="chooseIcon(icon)" :class="{'active':activeIndex === icon}"/>
</li>
</ul>
</a-tab-pane>
<a-tab-pane tab="编辑类图标" key="3">
<ul>
<li v-for="icon in icons.editIcons" :key="icon">
<a-icon :type="icon" :title="icon" @click="chooseIcon(icon)" :class="{'active':activeIndex === icon}"/>
</li>
</ul>
</a-tab-pane>
<a-tab-pane tab="数据类图标" key="4">
<ul>
<li v-for="icon in icons.dataIcons" :key="icon">
<a-icon :type="icon" :title="icon" @click="chooseIcon(icon)" :class="{'active':activeIndex === icon}"/>
</li>
</ul>
</a-tab-pane>
<a-tab-pane tab="网站通用图标" key="5">
<ul>
<li v-for="icon in icons.webIcons" :key="icon">
<a-icon :type="icon" :title="icon" @click="chooseIcon(icon)" :class="{'active':activeIndex === icon}"/>
</li>
</ul>
</a-tab-pane>
<a-tab-pane tab="品牌和标识" key="6">
<ul>
<li v-for="icon in icons.logoIcons" :key="icon">
<a-icon :type="icon" :title="icon" @click="chooseIcon(icon)" :class="{'active':activeIndex === icon}"/>
</li>
</ul>
</a-tab-pane>
</a-tabs>
</a-modal>
</template>
<script>
const directionIcons = ['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', 'up-circle-o', 'down-circle-o', 'right-circle-o', 'left-circle-o', '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', 'play-circle-o', 'up-square', 'down-square', 'left-square', 'right-square', 'up-square-o', 'down-square-o', 'left-square-o', 'right-square-o', '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', 'radius-upright', 'fullscreen', 'fullscreen-exit']
const suggestionIcons = ['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']
const editIcons = ['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']
const dataIcons = ['area-chart', 'pie-chart', 'bar-chart', 'dot-chart', 'line-chart', 'radar-chart', 'heat-map', 'fall', 'rise', 'stock', 'box-plot', 'fund', 'sliders']
const webIcons = ['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']
const logoIcons = ['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', 'amazon', 'google', 'codepen-circle', 'alipay', 'ant-design', 'aliyun', 'zhihu', 'slack', 'slack-square', 'behance', 'behance-square', 'dribbble', 'dribbble-square', 'instagram', 'yuque', 'alibaba', 'yahoo']
export default {
name: 'Icons',
props: {
iconChooseVisible: {
default: false
}
},
data () {
return {
icons: {
directionIcons,
suggestionIcons,
editIcons,
dataIcons,
webIcons,
logoIcons
},
choosedIcon: '',
activeIndex: ''
}
},
computed: {
show: {
get: function () {
return this.iconChooseVisible
},
set: function () {
}
}
},
methods: {
reset () {
this.activeIndex = ''
},
chooseIcon (icon) {
this.activeIndex = icon
this.choosedIcon = icon
this.$message.success(`选中 ${icon}`)
},
ok () {
if (this.choosedIcon === '') {
this.$message.warning('尚未选择任何图标')
return
}
this.reset()
this.$emit('choose', this.choosedIcon)
},
cancel () {
this.reset()
this.$emit('close')
}
}
}
</script>
<style lang="less" scoped>
@import "Icon";
</style>

View File

@ -0,0 +1,325 @@
<template>
<a-card :bordered="false" class="card-area">
<div :class="advanced ? 'search' : null">
<!-- 搜索区域 -->
<a-form layout="horizontal">
<div :class="advanced ? null: 'fold'">
<a-row >
<a-col :md="12" :sm="24" >
<a-form-item
label="名称"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<a-input v-model="queryParams.menuName"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" >
<a-form-item
label="创建时间"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<range-date @change="handleDateChange" ref="createTime"></range-date>
</a-form-item>
</a-col>
</a-row>
</div>
<span style="float: right; margin-top: 3px;">
<a-button type="primary" @click="search">查询</a-button>
<a-button style="margin-left: 8px" @click="reset">重置</a-button>
</span>
</a-form>
</div>
<div>
<div class="operator">
<a-popconfirm
title="请选择创建类型"
okText="按钮"
cancelText="菜单"
@cancel="() => createMenu()"
@confirm="() => createButton()">
<a-icon slot="icon" type="question-circle-o" style="color: orangered" />
<a-button type="primary" v-hasPermission="'menu:add'" ghost>新增</a-button>
</a-popconfirm>
<a-button v-hasPermission="'menu:delete'" @click="batchDelete">删除</a-button>
<a-dropdown v-hasPermission="'menu:export'">
<a-menu slot="overlay">
<a-menu-item key="export-data" @click="exprotExccel">导出Excel</a-menu-item>
</a-menu>
<a-button>
更多操作 <a-icon type="down" />
</a-button>
</a-dropdown>
</div>
<!-- 表格区域 -->
<a-table :columns="columns"
:key="key"
:dataSource="dataSource"
:pagination="pagination"
:loading="loading"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
@change="handleTableChange" :scroll="{ x: 1500 }">
<template slot="icon" slot-scope="text, record">
<a-icon :type="text" />
</template>
<template slot="operation" slot-scope="text, record">
<a-icon v-hasPermission="'menu:update'" type="setting" theme="twoTone" twoToneColor="#4a9ff5" @click="edit(record)" title="修改"></a-icon>
<a-badge v-hasNoPermission="'menu:update'" status="warning" text="无权限"></a-badge>
</template>
</a-table>
</div>
<!-- 新增菜单 -->
<menu-add
@close="handleMenuAddClose"
@success="handleMenuAddSuccess"
:menuAddVisiable="menuAddVisiable">
</menu-add>
<!-- 修改菜单 -->
<menu-edit
ref="menuEdit"
@close="handleMenuEditClose"
@success="handleMenuEditSuccess"
:menuEditVisiable="menuEditVisiable">
</menu-edit>
<!-- 新增按钮 -->
<button-add
@close="handleButtonAddClose"
@success="handleButtonAddSuccess"
:buttonAddVisiable="buttonAddVisiable">
</button-add>
<!-- 修改按钮 -->
<button-edit
ref="buttonEdit"
@close="handleButtonEditClose"
@success="handleButtonEditSuccess"
:buttonEditVisiable="buttonEditVisiable">
</button-edit>
</a-card>
</template>
<script>
import RangeDate from '@/components/datetime/RangeDate'
import MenuAdd from './MenuAdd'
import MenuEdit from './MenuEdit'
import ButtonAdd from './ButtonAdd'
import ButtonEdit from './ButtonEdit'
export default {
name: 'Menu',
components: {ButtonAdd, ButtonEdit, RangeDate, MenuAdd, MenuEdit},
data () {
return {
advanced: false,
key: +new Date(),
queryParams: {},
filteredInfo: null,
dataSource: [],
selectedRowKeys: [],
pagination: {
defaultPageSize: 10000000,
hideOnSinglePage: true,
indentSize: 100
},
loading: false,
menuAddVisiable: false,
menuEditVisiable: false,
buttonAddVisiable: false,
buttonEditVisiable: false
}
},
computed: {
columns () {
let {filteredInfo} = this
filteredInfo = filteredInfo || {}
return [{
title: '名称',
dataIndex: 'text',
width: 200,
fixed: 'left'
}, {
title: '图标',
dataIndex: 'icon',
scopedSlots: { customRender: 'icon' }
}, {
title: '类型',
dataIndex: 'type',
customRender: (text, row, index) => {
switch (text) {
case '0':
return <a-tag color="cyan"> 菜单 </a-tag>
case '1':
return <a-tag color="pink"> 按钮 </a-tag>
default:
return text
}
},
filters: [
{text: '按钮', value: '1'},
{text: '菜单', value: '0'}
],
filterMultiple: false,
filteredValue: filteredInfo.type || null,
onFilter: (value, record) => record.type.includes(value)
}, {
title: '地址',
dataIndex: 'path'
}, {
title: 'Vue组件',
dataIndex: 'component'
}, {
title: '权限',
dataIndex: 'permission'
}, {
title: '排序',
dataIndex: 'order'
}, {
title: '创建时间',
dataIndex: 'createTime'
}, {
title: '修改时间',
dataIndex: 'modifyTime'
}, {
title: '操作',
dataIndex: 'operation',
width: 120,
scopedSlots: {customRender: 'operation'},
fixed: 'right'
}]
}
},
mounted () {
this.fetch()
},
methods: {
onSelectChange (selectedRowKeys) {
this.selectedRowKeys = selectedRowKeys
},
handleMenuEditClose () {
this.menuEditVisiable = false
},
handleMenuEditSuccess () {
this.menuEditVisiable = false
this.$message.success('修改菜单成功')
this.fetch()
},
handleButtonEditClose () {
this.buttonEditVisiable = false
},
handleButtonEditSuccess () {
this.buttonEditVisiable = false
this.$message.success('修改按钮成功')
this.fetch()
},
edit (record) {
if (record.type === '0') {
this.$refs.menuEdit.setFormValues(record)
this.menuEditVisiable = true
} else {
this.$refs.buttonEdit.setFormValues(record)
this.buttonEditVisiable = true
}
},
handleButtonAddClose () {
this.buttonAddVisiable = false
},
handleButtonAddSuccess () {
this.buttonAddVisiable = false
this.$message.success('新增按钮成功')
this.fetch()
},
createButton () {
this.buttonAddVisiable = true
},
handleMenuAddClose () {
this.menuAddVisiable = false
},
handleMenuAddSuccess () {
this.menuAddVisiable = false
this.$message.success('新增菜单成功')
this.fetch()
},
createMenu () {
this.menuAddVisiable = true
},
handleDateChange (value) {
if (value) {
this.queryParams.createTimeFrom = value[0]
this.queryParams.createTimeTo = value[1]
}
},
batchDelete () {
if (!this.selectedRowKeys.length) {
this.$message.warning('请选择需要删除的记录')
return
}
let that = this
this.$confirm({
title: '确定删除所选中的记录?',
content: '当您点击确定按钮后,这些记录将会被彻底删除,如果其包含子记录,也将一并删除!',
centered: true,
onOk () {
that.$delete('menu/' + that.selectedRowKeys.join(',')).then(() => {
that.$message.success('删除成功')
that.selectedRowKeys = []
that.fetch()
})
},
onCancel () {
that.selectedRowKeys = []
}
})
},
exprotExccel () {
let {filteredInfo} = this
this.$export('menu/excel', {
...this.queryParams,
...filteredInfo
})
},
search () {
let {filteredInfo} = this
this.fetch({
...this.queryParams,
...filteredInfo
})
},
reset () {
//
this.selectedRowKeys = []
//
this.filteredInfo = null
//
this.queryParams = {}
//
this.$refs.createTime.reset()
this.fetch()
},
handleTableChange (pagination, filters, sorter) {
// Vue data使
this.filteredInfo = filters
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
...this.queryParams,
...filters
})
},
fetch (params = {}) {
this.loading = true
this.$get('menu', {
...params
}).then((r) => {
let data = r.data
this.loading = false
if (Object.is(data.rows.children, undefined)) {
this.dataSource = data.rows
} else {
this.dataSource = data.rows.children
}
})
}
}
}
</script>
<style lang="less" scoped>
@import "../../../../static/less/Common";
</style>

View File

@ -0,0 +1,198 @@
<template>
<a-drawer
title="新增菜单"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="menuAddVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='菜单名称' v-bind="formItemLayout">
<a-input v-model="menu.menuName"
v-decorator="['menuName',
{rules: [
{ required: true, message: '菜单名称不能为空'},
{ max: 10, message: '长度不能超过10个字符'}
]}]"/>
</a-form-item>
<a-form-item label='菜单URL'
v-bind="formItemLayout">
<a-input v-model="menu.path"
v-decorator="['path',
{rules: [
{ required: true, message: '菜单URL不能为空'},
{ max: 50, message: '长度不能超过50个字符'}
]}]"/>
</a-form-item>
<a-form-item label='组件地址'
v-bind="formItemLayout">
<a-input v-model="menu.component"
v-decorator="['component',
{rules: [
{ required: true, message: '组件地址不能为空'},
{ max: 100, message: '长度不能超过100个字符'}
]}]"/>
</a-form-item>
<a-form-item label='相关权限' v-bind="formItemLayout">
<a-input v-model="menu.perms"
v-decorator="['perms',
{rules: [
{ max: 50, message: '长度不能超过50个字符'}
]}]"/>
</a-form-item>
<a-form-item label='菜单图标'
v-bind="formItemLayout">
<a-input ref="icons" v-model="menu.icon" placeholder="点击右侧按钮选择图标">
<a-icon v-if="menu.icon" slot="suffix" type="close-circle" @click="deleteIcons"/>
<a-icon slot="addonAfter" type="setting" style="cursor: pointer" @click="chooseIcons"/>
</a-input>
</a-form-item>
<a-form-item label='菜单排序' v-bind="formItemLayout">
<a-input-number v-model="menu.orderNum" style="width: 100%"/>
</a-form-item>
<a-form-item label='上级菜单'
style="margin-bottom: 2rem"
v-bind="formItemLayout">
<a-tree
:key="menuTreeKey"
:checkable="true"
:checkStrictly="true"
@check="handleCheck"
@expand="handleExpand"
:expandedKeys="expandedKeys"
:treeData="menuTreeData">
</a-tree>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-dropdown style="float: left" :trigger="['click']" placement="topCenter">
<a-menu slot="overlay">
<a-menu-item key="1" @click="expandAll">展开所有</a-menu-item>
<a-menu-item key="2" @click="closeAll">合并所有</a-menu-item>
</a-menu>
<a-button>
树操作 <a-icon type="up" />
</a-button>
</a-dropdown>
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
<icons
@choose="handleIconChoose"
@close="handleIconCancel"
:iconChooseVisible="iconChooseVisible">
</icons>
</a-drawer>
</template>
<script>
import Icons from './Icons'
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
name: 'MenuAdd',
components: {Icons},
props: {
menuAddVisiable: {
default: false
}
},
data () {
return {
loading: false,
formItemLayout,
form: this.$form.createForm(this),
menuTreeKey: +new Date(),
menu: {
icon: ''
},
checkedKeys: [],
expandedKeys: [],
menuTreeData: [],
iconChooseVisible: false
}
},
methods: {
reset () {
this.loading = false
this.menuTreeKey = +new Date()
this.expandedKeys = this.checkedKeys = []
this.menu = {}
this.form.resetFields()
},
onClose () {
this.reset()
this.$emit('close')
},
handleCheck (checkedKeys) {
this.checkedKeys = checkedKeys
},
expandAll () {
this.expandedKeys = this.allTreeKeys
},
closeAll () {
this.expandedKeys = []
},
handleExpand (expandedKeys) {
this.expandedKeys = expandedKeys
},
chooseIcons () {
this.iconChooseVisible = true
},
handleIconCancel () {
this.iconChooseVisible = false
},
handleIconChoose (value) {
this.menu.icon = value
this.iconChooseVisible = false
},
deleteIcons () {
this.menu.icon = ''
},
handleSubmit () {
let checkedArr = Object.is(this.checkedKeys.checked, undefined) ? this.checkedKeys : this.checkedKeys.checked
if (checkedArr.length > 1) {
this.$message.error('最多只能选择一个上级菜单,请修改')
return
}
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
if (checkedArr.length) {
this.menu.parentId = checkedArr[0]
} else {
this.menu.parentId = ''
}
// 0 1
this.menu.type = '0'
this.$post('menu', {
...this.menu
}).then(() => {
this.reset()
this.$emit('success')
}).catch(() => {
this.loading = false
})
}
})
}
},
watch: {
menuAddVisiable () {
if (this.menuAddVisiable) {
this.$get('menu', {
type: '0'
}).then((r) => {
this.menuTreeData = r.data.rows.children
this.allTreeKeys = r.data.ids
})
}
}
}
}
</script>

View File

@ -0,0 +1,232 @@
<template>
<a-drawer
title="修改菜单"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="menuEditVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='菜单名称' v-bind="formItemLayout">
<a-input v-decorator="['menuName',
{rules: [
{ required: true, message: '菜单名称不能为空'},
{ max: 10, message: '长度不能超过10个字符'}
]}]"/>
</a-form-item>
<a-form-item label='菜单URL'
v-bind="formItemLayout">
<a-input v-decorator="['path',
{rules: [
{ required: true, message: '菜单URL不能为空'},
{ max: 50, message: '长度不能超过50个字符'}
]}]"/>
</a-form-item>
<a-form-item label='组件地址'
v-bind="formItemLayout">
<a-input v-decorator="['component',
{rules: [
{ required: true, message: '组件地址不能为空'},
{ max: 100, message: '长度不能超过100个字符'}
]}]"/>
</a-form-item>
<a-form-item label='相关权限' v-bind="formItemLayout">
<a-input v-decorator="['perms',
{rules: [
{ max: 50, message: '长度不能超过50个字符'}
]}]"/>
</a-form-item>
<a-form-item label='菜单图标'
v-decorator="['icon']"
v-bind="formItemLayout">
<a-input placeholder="点击右侧按钮选择图标" v-model="menu.icon">
<a-icon v-if="menu.icon" slot="suffix" type="close-circle" @click="deleteIcons"/>
<a-icon slot="addonAfter" type="setting" style="cursor: pointer" @click="chooseIcons"/>
</a-input>
</a-form-item>
<a-form-item label='菜单排序' v-bind="formItemLayout">
<a-input-number v-decorator="['orderNum']" style="width: 100%"/>
</a-form-item>
<a-form-item label='上级菜单'
style="margin-bottom: 2rem"
v-bind="formItemLayout">
<a-tree
ref="menuTree"
:key="menuTreeKey"
:checkable="true"
:checkStrictly="true"
@check="handleCheck"
@expand="handleExpand"
:expandedKeys="expandedKeys"
:defaultCheckedKeys="defaultCheckedKeys"
:treeData="menuTreeData">
</a-tree>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-dropdown style="float: left" :trigger="['click']" placement="topCenter">
<a-menu slot="overlay">
<a-menu-item key="1" @click="expandAll">展开所有</a-menu-item>
<a-menu-item key="2" @click="closeAll">合并所有</a-menu-item>
</a-menu>
<a-button>
树操作 <a-icon type="up" />
</a-button>
</a-dropdown>
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
<icons
@choose="handleIconChoose"
@close="handleIconCancel"
:iconChooseVisible="iconChooseVisible">
</icons>
</a-drawer>
</template>
<script>
import Icons from './Icons'
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
name: 'MenuEdit',
components: {Icons},
props: {
menuEditVisiable: {
default: false
}
},
data () {
return {
loading: false,
formItemLayout,
form: this.$form.createForm(this),
menuTreeKey: +new Date(),
menu: {
icon: ''
},
checkedKeys: [],
expandedKeys: [],
menuTreeData: [],
defaultCheckedKeys: [],
iconChooseVisible: false
}
},
methods: {
reset () {
this.loading = false
this.menuTreeKey = +new Date()
this.expandedKeys = this.checkedKeys = this.defaultCheckedKeys = []
this.menu = {}
this.form.resetFields()
},
onClose () {
this.reset()
this.$emit('close')
},
handleCheck (checkedKeys) {
this.checkedKeys = checkedKeys
},
expandAll () {
this.expandedKeys = this.allTreeKeys
},
closeAll () {
this.expandedKeys = []
},
handleExpand (expandedKeys) {
this.expandedKeys = expandedKeys
},
chooseIcons () {
this.iconChooseVisible = true
},
handleIconCancel () {
this.iconChooseVisible = false
},
handleIconChoose (value) {
this.menu.icon = value
this.iconChooseVisible = false
},
deleteIcons () {
this.menu.icon = ''
},
setFormValues ({...menu}) {
let fields = ['path', 'component', 'icon']
Object.keys(menu).forEach((key) => {
if (fields.indexOf(key) !== -1) {
this.form.getFieldDecorator(key)
let obj = {}
obj[key] = menu[key]
this.form.setFieldsValue(obj)
}
})
this.form.getFieldDecorator('menuName')
this.form.setFieldsValue({'menuName': menu.text})
this.form.getFieldDecorator('perms')
this.form.setFieldsValue({'perms': menu.permission})
this.form.getFieldDecorator('orderNum')
this.form.setFieldsValue({'orderNum': menu.order})
this.menu.icon = menu.icon
if (menu.parentId !== '0') {
this.defaultCheckedKeys.push(menu.parentId)
this.checkedKeys = this.defaultCheckedKeys
this.expandedKeys = this.checkedKeys
}
this.menu.menuId = menu.id
this.menuTreeKey = +new Date()
},
handleSubmit () {
let checkedArr = Object.is(this.checkedKeys.checked, undefined) ? this.checkedKeys : this.checkedKeys.checked
if (checkedArr.length > 1) {
this.$message.error('最多只能选择一个上级菜单,请修改')
return
}
if (checkedArr[0] === this.menu.menuId) {
this.$message.error('不能选择自己作为上级菜单,请修改')
return
}
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
let icon = this.menu.icon
let menu = this.form.getFieldsValue()
menu.icon = icon
menu.menuId = this.menu.menuId
if (checkedArr.length) {
menu.parentId = checkedArr[0]
} else {
menu.parentId = ''
}
// 0 1
menu.type = '0'
this.$put('menu', {
...menu
}).then(() => {
this.reset()
this.$emit('success')
}).catch(() => {
this.loading = false
})
}
})
}
},
watch: {
menuEditVisiable () {
if (this.menuEditVisiable) {
this.$get('menu', {
type: '0'
}).then((r) => {
this.menuTreeData = r.data.rows.children
this.allTreeKeys = r.data.ids
this.menuTreeKey = +new Date()
})
}
}
}
}
</script>

View File

@ -0,0 +1,343 @@
<template>
<a-card :bordered="false" class="card-area">
<div :class="advanced ? 'search' : null">
<!-- 搜索区域 -->
<a-form layout="horizontal">
<div :class="advanced ? null: 'fold'">
<a-row>
<a-col :md="12" :sm="24">
<a-form-item
label="资源名称"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<a-input v-model="queryParams.roleName"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24">
<a-form-item
label="创建时间"
:labelCol="{span: 5}"
:wrapperCol="{span: 18, offset: 1}">
<range-date @change="handleDateChange" ref="createTime"></range-date>
</a-form-item>
</a-col>
</a-row>
</div>
<span style="float: right; margin-top: 3px;">
<a-button type="primary" @click="search">查询</a-button>
<a-button style="margin-left: 8px" @click="reset">重置</a-button>
</span>
</a-form>
</div>
<div>
<div class="operator">
<a-button v-hasPermission="'resource:add'" ghost type="primary" @click="add">新增</a-button>
<a-button v-hasPermission="'resource:delete'" @click="batchDelete">删除</a-button>
<a-dropdown v-hasPermission="'resource:export'">
<a-menu slot="overlay">
<a-menu-item key="export-data" @click="exportExccel">导出Excel</a-menu-item>
</a-menu>
<a-button>
更多操作
<a-icon type="down"/>
</a-button>
</a-dropdown>
</div>
<!-- 表格区域 -->
<a-table ref="TableInfo"
:columns="columns"
:dataSource="dataSource"
:pagination="pagination"
:loading="loading"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:scroll="{ x: 900 }"
@change="handleTableChange">
<template slot="remark" slot-scope="text, record">
<a-popover placement="topLeft">
<template slot="content">
<div style="max-width: 200px">{{text}}</div>
</template>
<p style="width: 200px;margin-bottom: 0">{{text}}</p>
</a-popover>
</template>
<template slot="operation" slot-scope="text, record">
<a-icon type="eye" theme="twoTone" twoToneColor="#42b983" @click="view(record)" title="查看"></a-icon>
</template>
</a-table>
<!-- 新增资源 -->
<resource-add
@close="handleResourceAddClose"
@success="handleResourceAddSuccess"
:resourceAddVisiable="resourceAdd.visible">
</resource-add>
<resource-info
@close="handleResourceInfoClose"
:resourceInfoVisiable="resourceInfo.visible"
:resourceInfoData="resourceInfo.data">
</resource-info>
</div>
</a-card>
</template>
<script>
import RangeDate from '@/components/datetime/RangeDate'
import ResourceAdd from './ResourceAdd'
import ResourceInfo from './ResourceInfo'
export default {
name: 'resource',
components: {RangeDate, ResourceAdd, ResourceInfo},
data () {
return {
advanced: false,
resourceInfo: {
visible: false,
data: {}
},
resourceAdd: {
visible: false
},
queryParams: {
createTimeFrom: '',
createTimeTo: ''
},
filteredInfo: null,
dataSource: [],
sortedInfo: null,
selectedRowKeys: [],
pagination: {
pageSizeOptions: ['10', '20', '30', '40', '100'],
defaultCurrent: 1,
defaultPageSize: 10,
showQuickJumper: true,
showSizeChanger: true,
showTotal: (total, range) => `显示 ${range[0]} ~ ${range[1]} 条记录,共 ${total} 条记录`
},
loading: false
}
},
computed: {
columns () {
let { sortedInfo, filteredInfo } = this
sortedInfo = sortedInfo || {}
filteredInfo = filteredInfo || {}
return [{
title: '资源名称',
dataIndex: 'username',
sorter: true,
sortOrder: sortedInfo.columnKey === 'username' && sortedInfo.order
}, {
title: '性别',
dataIndex: 'ssex',
customRender: (text, row, index) => {
switch (text) {
case '0':
return '男'
case '1':
return '女'
case '2':
return '保密'
default:
return text
}
},
filters: [
{ text: '男', value: '0' },
{ text: '女', value: '1' },
{ text: '保密', value: '2' }
],
filterMultiple: false,
filteredValue: filteredInfo.ssex || null,
onFilter: (value, record) => record.ssex.includes(value)
}, {
title: '邮箱',
dataIndex: 'email',
scopedSlots: { customRender: 'email' },
width: 100
}, {
title: '部门',
dataIndex: 'deptName'
}, {
title: '电话',
dataIndex: 'mobile'
}, {
title: '状态',
dataIndex: 'status',
customRender: (text, row, index) => {
switch (text) {
case '0':
return <a-tag color="red">锁定</a-tag>
case '1':
return <a-tag color="cyan">有效</a-tag>
default:
return text
}
},
filters: [
{ text: '有效', value: '1' },
{ text: '锁定', value: '0' }
],
filterMultiple: false,
filteredValue: filteredInfo.status || null,
onFilter: (value, record) => record.status.includes(value)
}, {
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
sortOrder: sortedInfo.columnKey === 'createTime' && sortedInfo.order
}, {
title: '操作',
dataIndex: 'operation',
scopedSlots: { customRender: 'operation' }
}]
}
},
mounted () {
this.fetch()
},
methods: {
onSelectChange (selectedRowKeys) {
this.selectedRowKeys = selectedRowKeys
},
toggleAdvanced () {
this.advanced = !this.advanced
if (!this.advanced) {
this.queryParams.createTimeFrom = ''
this.queryParams.createTimeTo = ''
}
},
add () {
this.resourceAdd.visible = true
},
view (record) {
this.resourceInfo.data = record
this.resourceInfo.visible = true
},
handleResourceInfoClose () {
this.resourceInfo.visible = false
},
handleResourceAddClose () {
this.resourceAdd.visible = false
},
handleResourceAddSuccess () {
this.resourceAdd.visible = false
this.$message.success('新增资源成功')
this.fetch()
},
handleDateChange (value) {
if (value) {
this.queryParams.createTimeFrom = value[0]
this.queryParams.createTimeTo = value[1]
}
},
batchDelete () {
if (!this.selectedRowKeys.length) {
this.$message.warning('请选择需要删除的记录')
return
}
let that = this
this.$confirm({
title: '确定删除所选中的记录?',
content: '当您点击确定按钮后,这些记录将会被彻底删除,如果其包含子记录,也将一并删除!',
centered: true,
onOk () {
that.$delete('resource/' + that.selectedRowKeys.join(',')).then(() => {
that.$message.success('删除成功')
that.selectedRowKeys = []
that.fetch()
})
},
onCancel () {
that.selectedRowKeys = []
}
})
},
exportExccel () {
let {sortedInfo} = this
let sortField, sortOrder
//
if (sortedInfo) {
sortField = sortedInfo.field
sortOrder = sortedInfo.order
}
this.$export('resource/excel', {
sortField: sortField,
sortOrder: sortOrder,
...this.queryParams
})
},
search () {
let {sortedInfo} = this
let sortField, sortOrder
//
if (sortedInfo) {
sortField = sortedInfo.field
sortOrder = sortedInfo.order
}
this.fetch({
sortField: sortField,
sortOrder: sortOrder,
...this.queryParams
})
},
reset () {
//
this.selectedRowKeys = []
//
this.$refs.TableInfo.pagination.current = this.pagination.defaultCurrent
if (this.paginationInfo) {
this.paginationInfo.current = this.pagination.defaultCurrent
this.paginationInfo.pageSize = this.pagination.defaultPageSize
}
//
this.filteredInfo = null
//
this.sortedInfo = null
//
this.queryParams = {}
//
if (this.advanced) {
this.$refs.createTime.reset()
}
this.fetch()
},
handleTableChange (pagination, filters, sorter) {
// Vue data使
this.filteredInfo = filters
this.fetch({
sortField: sorter.field,
sortOrder: sorter.order,
...this.queryParams,
...filters
})
},
fetch (params = {}) {
this.loading = true
if (this.paginationInfo) {
//
this.$refs.TableInfo.pagination.current = this.paginationInfo.current
this.$refs.TableInfo.pagination.pageSize = this.paginationInfo.pageSize
params.pageSize = this.paginationInfo.pageSize
params.pageNum = this.paginationInfo.current
} else {
//
params.pageSize = this.pagination.defaultPageSize
params.pageNum = this.pagination.defaultCurrent
}
this.$get('user', {
...params
}).then((r) => {
let data = r.data
const pagination = {...this.pagination}
pagination.total = data.total
this.dataSource = data.rows
this.pagination = pagination
this.loading = false
})
}
}
}
</script>
<style lang="less" scoped>
@import "../../../../static/less/Common";
</style>

View File

@ -0,0 +1,162 @@
<template>
<a-drawer
title="新增资源"
:maskClosable="false"
width=650
placement="right"
:closable="false"
@close="onClose"
:visible="resourceAddVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<a-form :form="form">
<a-form-item label='资源名称'
v-bind="formItemLayout"
:validateStatus="validateStatus"
:help="help">
<a-input @blur="handleResourceNameBlur" v-model="resource.userName" v-decorator="['roleName']"/>
</a-form-item>
<a-form-item label='资源描述' v-bind="formItemLayout">
<a-textarea
:rows="4"
v-model="resource.remark"
v-decorator="[
'remark',
{rules: [
{ max: 100, message: '长度不能超过100个字符'}
]}]">
</a-textarea>
</a-form-item>
</a-form>
<div class="drawer-bootom-button">
<a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消">
<a-button style="margin-right: .8rem">取消</a-button>
</a-popconfirm>
<a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button>
</div>
</a-drawer>
</template>
<script>
const formItemLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 18 }
}
export default {
name: 'ResourceAdd',
props: {
resourceAddVisiable: {
default: false
}
},
data () {
return {
loading: false,
formItemLayout,
form: this.$form.createForm(this),
validateStatus: '',
menuSelectStatus: '',
help: '',
menuSelectHelp: '',
resource: {
userName: '',
remark: '',
menuId: ''
},
checkedKeys: [],
checkStrictly: true
}
},
methods: {
reset () {
this.validateStatus = this.help = ''
this.loading = false
this.form.resetFields()
},
onClose () {
this.reset()
this.$emit('close')
},
expandAll () {
this.expandedKeys = this.allTreeKeys
},
closeAll () {
this.expandedKeys = []
},
enableRelate () {
this.checkStrictly = false
},
disableRelate () {
this.checkStrictly = true
},
handleCheck (checkedKeys) {
this.checkedKeys = checkedKeys
let checkedArr = Object.is(checkedKeys.checked, undefined) ? checkedKeys : checkedKeys.checked
if (checkedArr.length) {
this.menuSelectStatus = ''
this.menuSelectHelp = ''
} else {
this.menuSelectStatus = 'error'
this.menuSelectHelp = '请选择相应的权限'
}
},
handleExpand (expandedKeys) {
this.expandedKeys = expandedKeys
},
handleSubmit () {
let checkedArr = Object.is(this.checkedKeys.checked, undefined) ? this.checkedKeys : this.checkedKeys.checked
if (this.validateStatus !== 'success') {
this.handleResourceNameBlur()
} else if (checkedArr.length === 0) {
this.menuSelectStatus = 'error'
this.menuSelectHelp = '请选择相应的权限'
} else {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true
this.resource.menuId = checkedArr.join(',')
this.$post('resource', {
...this.resource
}).then((r) => {
this.reset()
this.$emit('success')
}).catch(() => {
this.loading = false
})
}
})
}
},
handleResourceNameBlur () {
let resourceName = this.resource.resourceName.trim()
if (resourceName.length) {
if (resourceName.length > 10) {
this.validateStatus = 'error'
this.help = '资源名称不能超过10个字符'
} else {
this.validateStatus = 'validating'
this.$get(`role/check/${resourceName}`).then((r) => {
if (r.data) {
this.validateStatus = 'success'
this.help = ''
} else {
this.validateStatus = 'error'
this.help = '抱歉,该资源名称已存在'
}
})
}
} else {
this.validateStatus = 'error'
this.help = '资源名称不能为空'
}
}
},
watch: {
resourceAddVisiable () {
if (this.resourceAddVisiable) {
this.$get('resourceName').then((r) => {
this.allTreeKeys = r.data.ids
})
}
}
}
}
</script>

View File

@ -0,0 +1,44 @@
<template>
<a-drawer
title="角色信息"
:maskClosable="false"
width=650
placement="right"
:closable="true"
@close="close"
:visible="resourceInfoVisiable"
style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;">
<p><a-icon type="crown" />&nbsp;&nbsp;资源名称{{resourceInfoData.roleName}}</p>
<p :title="resourceInfoData.remark"><a-icon type="book" />&nbsp;&nbsp;资源描述{{resourceInfoData.remark}}</p>
<p><a-icon type="clock-circle" />&nbsp;&nbsp;创建时间{{resourceInfoData.createTime}}</p>
<p><a-icon type="clock-circle" />&nbsp;&nbsp;修改时间{{resourceInfoData.modifyTime? resourceInfoData.modifyTime : '暂未修改'}}</p>
</a-drawer>
</template>
<script>
export default {
name: 'ResourceInfo',
props: {
resourceInfoVisiable: {
require: true,
default: false
},
resourceInfoData: {
require: true
}
},
data () {
return {
key: +new Date(),
loading: true,
checkedKeys: [],
menuTreeData: []
}
},
methods: {
close () {
this.$emit('close')
this.checkedKeys = []
}
}
}
</script>

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