5.0
11
sop-admin/sop-admin-frontend/src/App.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
9
sop-admin/sop-admin-frontend/src/api/table.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params) {
|
||||
return request({
|
||||
url: '/table/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
24
sop-admin/sop-admin-frontend/src/api/user.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function login(data) {
|
||||
return request({
|
||||
url: '/user/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getInfo(token) {
|
||||
return request({
|
||||
url: '/user/info',
|
||||
method: 'get',
|
||||
params: { token }
|
||||
})
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return request({
|
||||
url: '/user/logout',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
|
||||
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
|
||||
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import pathToRegexp from 'path-to-regexp'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
levelList: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.getBreadcrumb()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getBreadcrumb()
|
||||
},
|
||||
methods: {
|
||||
getBreadcrumb() {
|
||||
// only show routes with meta.title
|
||||
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
|
||||
const first = matched[0]
|
||||
|
||||
if (!this.isDashboard(first)) {
|
||||
matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
|
||||
}
|
||||
|
||||
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
||||
},
|
||||
isDashboard(route) {
|
||||
const name = route && route.name
|
||||
if (!name) {
|
||||
return false
|
||||
}
|
||||
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
|
||||
},
|
||||
pathCompile(path) {
|
||||
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
|
||||
const { params } = this.$route
|
||||
var toPath = pathToRegexp.compile(path)
|
||||
return toPath(params)
|
||||
},
|
||||
handleLink(item) {
|
||||
const { redirect, path } = item
|
||||
if (redirect) {
|
||||
this.$router.push(redirect)
|
||||
return
|
||||
}
|
||||
this.$router.push(this.pathCompile(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-breadcrumb.el-breadcrumb {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
margin-left: 8px;
|
||||
|
||||
.no-redirect {
|
||||
color: #97a8be;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div style="padding: 0 15px;" @click="toggleClick">
|
||||
<svg
|
||||
:class="{'is-active':isActive}"
|
||||
class="hamburger"
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
>
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Hamburger',
|
||||
props: {
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleClick() {
|
||||
this.$emit('toggleClick')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hamburger {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.hamburger.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<svg :class="svgClass" aria-hidden="true" v-on="$listeners">
|
||||
<use :xlink:href="iconName" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SvgIcon',
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return `#icon-${this.iconClass}`
|
||||
},
|
||||
svgClass() {
|
||||
if (this.className) {
|
||||
return 'svg-icon ' + this.className
|
||||
} else {
|
||||
return 'svg-icon'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
9
sop-admin/sop-admin-frontend/src/icons/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Vue from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon'// svg component
|
||||
|
||||
// register globally
|
||||
Vue.component('svg-icon', SvgIcon)
|
||||
|
||||
const req = require.context('./svg', false, /\.svg$/)
|
||||
const requireAll = requireContext => requireContext.keys().map(requireContext)
|
||||
requireAll(req)
|
1
sop-admin/sop-admin-frontend/src/icons/svg/dashboard.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>
|
After Width: | Height: | Size: 2.3 KiB |
1
sop-admin/sop-admin-frontend/src/icons/svg/example.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>
|
After Width: | Height: | Size: 497 B |
1
sop-admin/sop-admin-frontend/src/icons/svg/eye-open.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
sop-admin/sop-admin-frontend/src/icons/svg/eye.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
|
After Width: | Height: | Size: 944 B |
1
sop-admin/sop-admin-frontend/src/icons/svg/form.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>
|
After Width: | Height: | Size: 2.4 KiB |
1
sop-admin/sop-admin-frontend/src/icons/svg/link.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>
|
After Width: | Height: | Size: 285 B |
1
sop-admin/sop-admin-frontend/src/icons/svg/nested.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>
|
After Width: | Height: | Size: 821 B |
1
sop-admin/sop-admin-frontend/src/icons/svg/password.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>
|
After Width: | Height: | Size: 623 B |
1
sop-admin/sop-admin-frontend/src/icons/svg/table.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>
|
After Width: | Height: | Size: 597 B |
1
sop-admin/sop-admin-frontend/src/icons/svg/tree.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>
|
After Width: | Height: | Size: 1.8 KiB |
1
sop-admin/sop-admin-frontend/src/icons/svg/user.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>
|
After Width: | Height: | Size: 440 B |
22
sop-admin/sop-admin-frontend/src/icons/svgo.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
# replace default config
|
||||
|
||||
# multipass: true
|
||||
# full: true
|
||||
|
||||
plugins:
|
||||
|
||||
# - name
|
||||
#
|
||||
# or:
|
||||
# - name: false
|
||||
# - name: true
|
||||
#
|
||||
# or:
|
||||
# - name:
|
||||
# param1: 1
|
||||
# param2: 2
|
||||
|
||||
- removeAttrs:
|
||||
attrs:
|
||||
- 'fill'
|
||||
- 'fill-rule'
|
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<router-view :key="key" />
|
||||
</transition>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppMain',
|
||||
computed: {
|
||||
key() {
|
||||
return this.$route.fullPath
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-main {
|
||||
/*50 = navbar */
|
||||
min-height: calc(100vh - 50px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.fixed-header+.app-main {
|
||||
padding-top: 50px;
|
||||
}
|
||||
</style>
|
129
sop-admin/sop-admin-frontend/src/layout/components/Navbar.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||||
|
||||
<breadcrumb class="breadcrumb-container" />
|
||||
|
||||
<div class="right-menu">
|
||||
<el-button type="text" style="margin-right: 10px" @click="doLogout">退出</el-button>
|
||||
<!--<el-dropdown class="avatar-container" trigger="click">-->
|
||||
<!--<div class="avatar-wrapper">-->
|
||||
<!--<img src="https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80'" class="user-avatar">-->
|
||||
<!--<i class="user-avatar el-icon-s-custom"></i>-->
|
||||
<!--</div>-->
|
||||
<!--<el-dropdown-menu slot="dropdown" class="user-dropdown">-->
|
||||
<!--<el-dropdown-item>-->
|
||||
<!--<span style="display:block;" @click="logout">退出</span>-->
|
||||
<!--</el-dropdown-item>-->
|
||||
<!--</el-dropdown-menu>-->
|
||||
<!--</el-dropdown>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Breadcrumb,
|
||||
Hamburger
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar',
|
||||
'avatar'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
toggleSideBar() {
|
||||
this.$store.dispatch('app/toggleSideBar')
|
||||
},
|
||||
doLogout() {
|
||||
this.logout()
|
||||
// this.$router.push(`/login?redirect=${this.$route.fullPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||
|
||||
.hamburger-container {
|
||||
line-height: 46px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
-webkit-tap-highlight-color:transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
float: right;
|
||||
height: 100%;
|
||||
line-height: 50px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.right-menu-item {
|
||||
display: inline-block;
|
||||
padding: 0 8px;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
color: #5a5e66;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
&.hover-effect {
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
margin-right: 30px;
|
||||
|
||||
.avatar-wrapper {
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.el-icon-caret-bottom {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 25px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,26 @@
|
||||
export default {
|
||||
computed: {
|
||||
device() {
|
||||
return this.$store.state.app.device
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
|
||||
// https://github.com/PanJiaChen/vue-element-admin/issues/1135
|
||||
this.fixBugIniOS()
|
||||
},
|
||||
methods: {
|
||||
fixBugIniOS() {
|
||||
const $subMenu = this.$refs.subMenu
|
||||
if ($subMenu) {
|
||||
const handleMouseleave = $subMenu.handleMouseleave
|
||||
$subMenu.handleMouseleave = (e) => {
|
||||
if (this.device === 'mobile') {
|
||||
return
|
||||
}
|
||||
handleMouseleave(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'MenuItem',
|
||||
functional: true,
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
render(h, context) {
|
||||
const { icon, title } = context.props
|
||||
const vnodes = []
|
||||
|
||||
if (icon) {
|
||||
vnodes.push(<svg-icon icon-class={icon}/>)
|
||||
}
|
||||
|
||||
if (title) {
|
||||
vnodes.push(<span slot='title'>{(title)}</span>)
|
||||
}
|
||||
return vnodes
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,36 @@
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable vue/require-component-is -->
|
||||
<component v-bind="linkProps(to)">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isExternal } from '@/utils/validate'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkProps(url) {
|
||||
if (isExternal(url)) {
|
||||
return {
|
||||
is: 'a',
|
||||
href: url,
|
||||
target: '_blank',
|
||||
rel: 'noopener'
|
||||
}
|
||||
}
|
||||
return {
|
||||
is: 'router-link',
|
||||
to: url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo">
|
||||
<h1 v-else class="sidebar-title">{{ title }} </h1>
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo">
|
||||
<h1 class="sidebar-title">{{ title }} </h1>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SidebarLogo',
|
||||
props: {
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: 'SOP Admin',
|
||||
logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-enter,
|
||||
.sidebarLogoFade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar-logo-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
background: #2b2f3a;
|
||||
/*text-align: center;*/
|
||||
padding-left: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
& .sidebar-logo-link {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& .sidebar-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
& .sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
line-height: 50px;
|
||||
font-size: 14px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.collapse {
|
||||
.sidebar-logo {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden" class="menu-wrapper">
|
||||
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
||||
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
|
||||
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
||||
<template slot="title">
|
||||
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
|
||||
</template>
|
||||
<sidebar-item
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:is-nest="true"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
class="nest-menu"
|
||||
/>
|
||||
</el-submenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import path from 'path'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
import Item from './Item'
|
||||
import AppLink from './Link'
|
||||
import FixiOSBug from './FixiOSBug'
|
||||
|
||||
export default {
|
||||
name: 'SidebarItem',
|
||||
components: { Item, AppLink },
|
||||
mixins: [FixiOSBug],
|
||||
props: {
|
||||
// route object
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
|
||||
// TODO: refactor with render function
|
||||
this.onlyOneChild = null
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
hasOneShowingChild(children = [], parent) {
|
||||
const showingChildren = children.filter(item => {
|
||||
if (item.hidden) {
|
||||
return false
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
this.onlyOneChild = item
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// When there is only one child router, the child router is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (showingChildren.length === 0) {
|
||||
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
resolvePath(routePath) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (isExternal(this.basePath)) {
|
||||
return this.basePath
|
||||
}
|
||||
return path.resolve(this.basePath, routePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div :class="{'has-logo':showLogo}">
|
||||
<logo v-if="showLogo" :collapse="isCollapse" />
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
:background-color="variables.menuBg"
|
||||
:text-color="variables.menuText"
|
||||
:unique-opened="false"
|
||||
:active-text-color="variables.menuActiveText"
|
||||
:collapse-transition="false"
|
||||
:default-openeds="opened"
|
||||
mode="vertical"
|
||||
>
|
||||
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Logo from './Logo'
|
||||
import SidebarItem from './SidebarItem'
|
||||
import variables from '@/styles/variables.scss'
|
||||
|
||||
export default {
|
||||
components: { SidebarItem, Logo },
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar'
|
||||
]),
|
||||
routes() {
|
||||
return this.$router.options.routes
|
||||
},
|
||||
opened() {
|
||||
return this.routes.filter(route => {
|
||||
return route.meta && route.meta.open
|
||||
}).map(route => {
|
||||
return route.path
|
||||
})
|
||||
},
|
||||
activeMenu() {
|
||||
const route = this.$route
|
||||
const { meta, path } = route
|
||||
// if set path, the sidebar will highlight the path you set
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
return path
|
||||
},
|
||||
showLogo() {
|
||||
return this.$store.state.settings.sidebarLogo
|
||||
},
|
||||
variables() {
|
||||
return variables
|
||||
},
|
||||
isCollapse() {
|
||||
return !this.sidebar.opened
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,3 @@
|
||||
export { default as Navbar } from './Navbar'
|
||||
export { default as Sidebar } from './Sidebar'
|
||||
export { default as AppMain } from './AppMain'
|
93
sop-admin/sop-admin-frontend/src/layout/index.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar class="sidebar-container" />
|
||||
<div class="main-container">
|
||||
<div :class="{'fixed-header':fixedHeader}">
|
||||
<navbar />
|
||||
</div>
|
||||
<app-main />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Navbar, Sidebar, AppMain } from './components'
|
||||
import ResizeMixin from './mixin/ResizeHandler'
|
||||
|
||||
export default {
|
||||
name: 'Layout',
|
||||
components: {
|
||||
Navbar,
|
||||
Sidebar,
|
||||
AppMain
|
||||
},
|
||||
mixins: [ResizeMixin],
|
||||
computed: {
|
||||
sidebar() {
|
||||
return this.$store.state.app.sidebar
|
||||
},
|
||||
device() {
|
||||
return this.$store.state.app.device
|
||||
},
|
||||
fixedHeader() {
|
||||
return this.$store.state.settings.fixedHeader
|
||||
},
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === 'mobile'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/styles/mixin.scss";
|
||||
@import "~@/styles/variables.scss";
|
||||
|
||||
.app-wrapper {
|
||||
@include clearfix;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
&.mobile.openSidebar{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
.drawer-bg {
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$sideBarWidth});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px)
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,45 @@
|
||||
import store from '@/store'
|
||||
|
||||
const { body } = document
|
||||
const WIDTH = 992 // refer to Bootstrap's responsive design
|
||||
|
||||
export default {
|
||||
watch: {
|
||||
$route(route) {
|
||||
if (this.device === 'mobile' && this.sidebar.opened) {
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
window.addEventListener('resize', this.$_resizeHandler)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.$_resizeHandler)
|
||||
},
|
||||
mounted() {
|
||||
const isMobile = this.$_isMobile()
|
||||
if (isMobile) {
|
||||
store.dispatch('app/toggleDevice', 'mobile')
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: true })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// use $_ for mixins properties
|
||||
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
|
||||
$_isMobile() {
|
||||
const rect = body.getBoundingClientRect()
|
||||
return rect.width - 1 < WIDTH
|
||||
},
|
||||
$_resizeHandler() {
|
||||
if (!document.hidden) {
|
||||
const isMobile = this.$_isMobile()
|
||||
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
|
||||
|
||||
if (isMobile) {
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
sop-admin/sop-admin-frontend/src/main.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
|
||||
|
||||
import ElementUI from 'element-ui'
|
||||
import 'element-ui/lib/theme-chalk/index.css'
|
||||
import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
|
||||
|
||||
import '@/styles/index.scss' // global css
|
||||
|
||||
import App from './App'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
|
||||
import '@/icons' // icon
|
||||
import '@/permission' // permission control
|
||||
import '@/utils/global' // 自定义全局js
|
||||
|
||||
/**
|
||||
* If you don't want to use mock-server
|
||||
* you want to use mockjs for request interception
|
||||
* you can execute:
|
||||
*
|
||||
* import { mockXHR } from '../mock'
|
||||
* mockXHR()
|
||||
*/
|
||||
|
||||
// set ElementUI lang to EN
|
||||
Vue.use(ElementUI, { locale })
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
})
|
65
sop-admin/sop-admin-frontend/src/permission.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import router from './router'
|
||||
// import store from './store'
|
||||
// import { Message } from 'element-ui'
|
||||
import NProgress from 'nprogress' // progress bar
|
||||
import 'nprogress/nprogress.css' // progress bar style
|
||||
import { getToken } from '@/utils/auth' // get token from cookie
|
||||
import getPageTitle from '@/utils/get-page-title'
|
||||
|
||||
NProgress.configure({ showSpinner: false }) // NProgress Configuration
|
||||
|
||||
const whiteList = ['/login'] // no redirect whitelist
|
||||
|
||||
router.beforeEach(async(to, from, next) => {
|
||||
// start progress bar
|
||||
NProgress.start()
|
||||
|
||||
// set page title
|
||||
document.title = getPageTitle(to.meta.title)
|
||||
|
||||
// determine whether the user has logged in
|
||||
const hasToken = getToken()
|
||||
|
||||
if (hasToken) {
|
||||
if (to.path === '/login') {
|
||||
// if is logged in, redirect to the home page
|
||||
next({ path: '/' })
|
||||
NProgress.done()
|
||||
} else {
|
||||
next()
|
||||
// const hasGetUserInfo = store.getters.name
|
||||
// if (hasGetUserInfo) {
|
||||
// next()
|
||||
// } else {
|
||||
// try {
|
||||
// // get user info
|
||||
// await store.dispatch('user/getInfo')
|
||||
//
|
||||
// next()
|
||||
// } catch (error) {
|
||||
// // remove token and go to login page to re-login
|
||||
// await store.dispatch('user/resetToken')
|
||||
// Message.error(error || 'Has Error')
|
||||
// next(`/login?redirect=${to.path}`)
|
||||
// NProgress.done()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
/* has no token*/
|
||||
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
// in the free login whitelist, go directly
|
||||
next()
|
||||
} else {
|
||||
// other pages that do not have permission to access are redirected to the login page.
|
||||
next(`/login?redirect=${to.path}`)
|
||||
NProgress.done()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
// finish progress bar
|
||||
NProgress.done()
|
||||
})
|
148
sop-admin/sop-admin-frontend/src/router/index.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
/* Layout */
|
||||
import Layout from '@/layout'
|
||||
|
||||
/**
|
||||
* Note: sub-menu only appear when route children.length >= 1
|
||||
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
|
||||
*
|
||||
* hidden: true if set true, item will not show in the sidebar(default is false)
|
||||
* alwaysShow: true if set true, will always show the root menu
|
||||
* if not set alwaysShow, when item has more than one children route,
|
||||
* it will becomes nested mode, otherwise not show the root menu
|
||||
* redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
|
||||
* name:'router-name' the name is used by <keep-alive> (must set!!!)
|
||||
* meta : {
|
||||
roles: ['admin','editor'] control the page roles (you can set multiple roles)
|
||||
title: 'title' the name show in sidebar and breadcrumb (recommend set)
|
||||
icon: 'svg-name' the icon show in the sidebar
|
||||
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
|
||||
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* constantRoutes
|
||||
* a base page that does not have permission requirements
|
||||
* all roles can be accessed
|
||||
*/
|
||||
export const constantRoutes = [
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/views/login/index'),
|
||||
hidden: true
|
||||
},
|
||||
|
||||
{
|
||||
path: '/404',
|
||||
component: () => import('@/views/404'),
|
||||
hidden: true
|
||||
},
|
||||
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/index'),
|
||||
meta: { title: '首页', icon: 'dashboard' }
|
||||
}]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/service',
|
||||
component: Layout,
|
||||
name: 'Service',
|
||||
meta: { title: '服务管理', icon: 'example', open: true },
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'ServiceList',
|
||||
component: () => import('@/views/service/serviceList'),
|
||||
meta: { title: '服务列表' }
|
||||
},
|
||||
{
|
||||
path: 'route',
|
||||
name: 'Route',
|
||||
component: () => import('@/views/service/route'),
|
||||
meta: { title: '路由管理' }
|
||||
},
|
||||
{
|
||||
path: 'monitor',
|
||||
name: 'Monitor',
|
||||
component: () => import('@/views/service/monitorNew'),
|
||||
meta: { title: '路由监控' }
|
||||
},
|
||||
{
|
||||
path: 'limit',
|
||||
name: 'Limit',
|
||||
component: () => import('@/views/service/limit'),
|
||||
meta: { title: '限流管理' }
|
||||
},
|
||||
{
|
||||
path: 'blacklist',
|
||||
name: 'Blacklist',
|
||||
component: () => import('@/views/service/ipBlacklist'),
|
||||
meta: { title: 'IP黑名单' }
|
||||
},
|
||||
{
|
||||
path: 'sdk',
|
||||
name: 'Sdk',
|
||||
component: () => import('@/views/service/sdk'),
|
||||
meta: { title: 'SDK管理' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/isv',
|
||||
component: Layout,
|
||||
name: 'Isv',
|
||||
meta: { title: 'ISV管理', icon: 'user', open: true },
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'IsvList',
|
||||
component: () => import('@/views/isv/index'),
|
||||
meta: { title: 'ISV列表' }
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
name: 'Role',
|
||||
component: () => import('@/views/isv/role'),
|
||||
meta: { title: '角色管理' }
|
||||
},
|
||||
{
|
||||
path: 'keys',
|
||||
name: 'Keys',
|
||||
component: () => import('@/views/isv/keys'),
|
||||
hidden: true,
|
||||
meta: { title: '秘钥管理' }
|
||||
}
|
||||
]
|
||||
},
|
||||
// 404 page must be placed at the end !!!
|
||||
{ path: '*', redirect: '/404', hidden: true }
|
||||
];
|
||||
|
||||
const createRouter = () => new Router({
|
||||
// mode: 'history', // require service support
|
||||
scrollBehavior: () => ({ y: 0 }),
|
||||
routes: constantRoutes
|
||||
});
|
||||
|
||||
const router = createRouter();
|
||||
|
||||
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
|
||||
export function resetRouter() {
|
||||
const newRouter = createRouter();
|
||||
router.matcher = newRouter.matcher // reset router
|
||||
}
|
||||
|
||||
export default router
|
16
sop-admin/sop-admin-frontend/src/settings.js
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
|
||||
title: 'SOP Admin',
|
||||
|
||||
/**
|
||||
* @type {boolean} true | false
|
||||
* @description Whether fix the header
|
||||
*/
|
||||
fixedHeader: false,
|
||||
|
||||
/**
|
||||
* @type {boolean} true | false
|
||||
* @description Whether show the logo in sidebar
|
||||
*/
|
||||
sidebarLogo: true
|
||||
}
|
8
sop-admin/sop-admin-frontend/src/store/getters.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const getters = {
|
||||
sidebar: state => state.app.sidebar,
|
||||
device: state => state.app.device,
|
||||
token: state => state.user.token,
|
||||
avatar: state => state.user.avatar,
|
||||
name: state => state.user.name
|
||||
}
|
||||
export default getters
|
19
sop-admin/sop-admin-frontend/src/store/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import getters from './getters'
|
||||
import app from './modules/app'
|
||||
import settings from './modules/settings'
|
||||
import user from './modules/user'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
app,
|
||||
settings,
|
||||
user
|
||||
},
|
||||
getters
|
||||
})
|
||||
|
||||
export default store
|
48
sop-admin/sop-admin-frontend/src/store/modules/app.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
const state = {
|
||||
sidebar: {
|
||||
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
|
||||
withoutAnimation: false
|
||||
},
|
||||
device: 'desktop'
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
TOGGLE_SIDEBAR: state => {
|
||||
state.sidebar.opened = !state.sidebar.opened
|
||||
state.sidebar.withoutAnimation = false
|
||||
if (state.sidebar.opened) {
|
||||
Cookies.set('sidebarStatus', 1)
|
||||
} else {
|
||||
Cookies.set('sidebarStatus', 0)
|
||||
}
|
||||
},
|
||||
CLOSE_SIDEBAR: (state, withoutAnimation) => {
|
||||
Cookies.set('sidebarStatus', 0)
|
||||
state.sidebar.opened = false
|
||||
state.sidebar.withoutAnimation = withoutAnimation
|
||||
},
|
||||
TOGGLE_DEVICE: (state, device) => {
|
||||
state.device = device
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
toggleSideBar({ commit }) {
|
||||
commit('TOGGLE_SIDEBAR')
|
||||
},
|
||||
closeSideBar({ commit }, { withoutAnimation }) {
|
||||
commit('CLOSE_SIDEBAR', withoutAnimation)
|
||||
},
|
||||
toggleDevice({ commit }, device) {
|
||||
commit('TOGGLE_DEVICE', device)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
31
sop-admin/sop-admin-frontend/src/store/modules/settings.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import defaultSettings from '@/settings'
|
||||
|
||||
const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
|
||||
|
||||
const state = {
|
||||
showSettings: showSettings,
|
||||
fixedHeader: fixedHeader,
|
||||
sidebarLogo: sidebarLogo
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
CHANGE_SETTING: (state, { key, value }) => {
|
||||
if (state.hasOwnProperty(key)) {
|
||||
state[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
changeSetting({ commit }, data) {
|
||||
commit('CHANGE_SETTING', data)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
|
90
sop-admin/sop-admin-frontend/src/store/modules/user.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { login, logout, getInfo } from '@/api/user'
|
||||
import { getToken, setToken, removeToken } from '@/utils/auth'
|
||||
import { resetRouter } from '@/router'
|
||||
|
||||
const state = {
|
||||
token: getToken(),
|
||||
name: '',
|
||||
avatar: ''
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_TOKEN: (state, token) => {
|
||||
state.token = token
|
||||
},
|
||||
SET_NAME: (state, name) => {
|
||||
state.name = name
|
||||
},
|
||||
SET_AVATAR: (state, avatar) => {
|
||||
state.avatar = avatar
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
// user login
|
||||
login({ commit }, userInfo) {
|
||||
const { username, password } = userInfo
|
||||
return new Promise((resolve, reject) => {
|
||||
login({ username: username.trim(), password: password }).then(response => {
|
||||
const { data } = response
|
||||
commit('SET_TOKEN', data.token)
|
||||
setToken(data.token)
|
||||
resolve()
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// get user info
|
||||
getInfo({ commit, state }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getInfo(state.token).then(response => {
|
||||
const { data } = response
|
||||
|
||||
if (!data) {
|
||||
reject('Verification failed, please Login again.')
|
||||
}
|
||||
|
||||
const { name, avatar } = data
|
||||
|
||||
commit('SET_NAME', name)
|
||||
commit('SET_AVATAR', avatar)
|
||||
resolve(data)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// user logout
|
||||
logout({ commit, state }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
logout(state.token).then(() => {
|
||||
commit('SET_TOKEN', '')
|
||||
removeToken()
|
||||
resetRouter()
|
||||
resolve()
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// remove token
|
||||
resetToken({ commit }) {
|
||||
return new Promise(resolve => {
|
||||
commit('SET_TOKEN', '')
|
||||
removeToken()
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
|
44
sop-admin/sop-admin-frontend/src/styles/element-ui.scss
Normal file
@@ -0,0 +1,44 @@
|
||||
// cover some element-ui styles
|
||||
|
||||
.el-breadcrumb__inner,
|
||||
.el-breadcrumb__inner a {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.el-upload {
|
||||
input[type="file"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
// to fixed https://github.com/ElemeFE/element/issues/2461
|
||||
.el-dialog {
|
||||
transform: none;
|
||||
left: 0;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// refine element ui upload
|
||||
.upload-container {
|
||||
.el-upload {
|
||||
width: 100%;
|
||||
|
||||
.el-upload-dragger {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown
|
||||
.el-dropdown-menu {
|
||||
a {
|
||||
display: block
|
||||
}
|
||||
}
|
68
sop-admin/sop-admin-frontend/src/styles/index.scss
Normal file
@@ -0,0 +1,68 @@
|
||||
@import './variables.scss';
|
||||
@import './mixin.scss';
|
||||
@import './transition.scss';
|
||||
@import './element-ui.scss';
|
||||
@import './sidebar.scss';
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
&:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// main-container global css
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cell .el-button {padding: 0;}
|
||||
span.tip {color: #909399;font-size: 12px;}
|
28
sop-admin/sop-admin-frontend/src/styles/mixin.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@mixin clearfix {
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin scrollBar {
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: #d3dce6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin relative {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
213
sop-admin/sop-admin-frontend/src/styles/sidebar.scss
Normal file
@@ -0,0 +1,213 @@
|
||||
#app {
|
||||
|
||||
.main-container {
|
||||
min-height: 100%;
|
||||
transition: margin-left .28s;
|
||||
margin-left: $sideBarWidth;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
transition: width 0.28s;
|
||||
width: $sideBarWidth !important;
|
||||
background-color: $menuBg;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
font-size: 0px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
|
||||
// reset element-ui css
|
||||
.horizontal-collapse-transition {
|
||||
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
|
||||
}
|
||||
|
||||
.scrollbar-wrapper {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.el-scrollbar__bar.is-vertical {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.has-logo {
|
||||
.el-scrollbar {
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
// menu hover
|
||||
.submenu-title-noDropdown,
|
||||
.el-submenu__title {
|
||||
&:hover {
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active>.el-submenu__title {
|
||||
color: $subMenuActiveText !important;
|
||||
}
|
||||
|
||||
& .nest-menu .el-submenu>.el-submenu__title,
|
||||
& .el-submenu .el-menu-item {
|
||||
min-width: $sideBarWidth !important;
|
||||
background-color: $subMenuBg !important;
|
||||
|
||||
&:hover {
|
||||
background-color: $subMenuHover !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
.sidebar-container {
|
||||
width: 54px !important;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin-left: 54px;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.submenu-title-noDropdown {
|
||||
padding: 0 !important;
|
||||
position: relative;
|
||||
|
||||
.el-tooltip {
|
||||
padding: 0 !important;
|
||||
|
||||
.svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu {
|
||||
overflow: hidden;
|
||||
|
||||
&>.el-submenu__title {
|
||||
padding: 0 !important;
|
||||
|
||||
.svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.el-submenu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
.el-submenu {
|
||||
&>.el-submenu__title {
|
||||
&>span {
|
||||
height: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse .el-menu .el-submenu {
|
||||
min-width: $sideBarWidth !important;
|
||||
}
|
||||
|
||||
// mobile responsive
|
||||
.mobile {
|
||||
.main-container {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
transition: transform .28s;
|
||||
width: $sideBarWidth !important;
|
||||
}
|
||||
|
||||
&.hideSidebar {
|
||||
.sidebar-container {
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
transform: translate3d(-$sideBarWidth, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.withoutAnimation {
|
||||
|
||||
.main-container,
|
||||
.sidebar-container {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// when menu collapsed
|
||||
.el-menu--vertical {
|
||||
&>.el-menu {
|
||||
.svg-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.nest-menu .el-submenu>.el-submenu__title,
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
// you can use $subMenuHover
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
// the scroll bar appears when the subMenu is too long
|
||||
>.el-menu--popup {
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: #d3dce6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
48
sop-admin/sop-admin-frontend/src/styles/transition.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
// global transition css
|
||||
|
||||
/* fade */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.28s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* fade-transform */
|
||||
.fade-transform-leave-active,
|
||||
.fade-transform-enter-active {
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.fade-transform-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* breadcrumb transition */
|
||||
.breadcrumb-enter-active,
|
||||
.breadcrumb-leave-active {
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.breadcrumb-enter,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.breadcrumb-move {
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.breadcrumb-leave-active {
|
||||
position: absolute;
|
||||
}
|
25
sop-admin/sop-admin-frontend/src/styles/variables.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
// sidebar
|
||||
$menuText:#bfcbd9;
|
||||
$menuActiveText:#409EFF;
|
||||
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
|
||||
|
||||
$menuBg:#304156;
|
||||
$menuHover:#263445;
|
||||
|
||||
$subMenuBg:#1f2d3d;
|
||||
$subMenuHover:#001528;
|
||||
|
||||
$sideBarWidth: 210px;
|
||||
|
||||
// the :export directive is the magic sauce for webpack
|
||||
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
|
||||
:export {
|
||||
menuText: $menuText;
|
||||
menuActiveText: $menuActiveText;
|
||||
subMenuActiveText: $subMenuActiveText;
|
||||
menuBg: $menuBg;
|
||||
menuHover: $menuHover;
|
||||
subMenuBg: $subMenuBg;
|
||||
subMenuHover: $subMenuHover;
|
||||
sideBarWidth: $sideBarWidth;
|
||||
}
|
15
sop-admin/sop-admin-frontend/src/utils/auth.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
const TokenKey = 'sop-admin-token'
|
||||
|
||||
export function getToken() {
|
||||
return Cookies.get(TokenKey)
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
return Cookies.set(TokenKey, token)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
return Cookies.remove(TokenKey)
|
||||
}
|
10
sop-admin/sop-admin-frontend/src/utils/get-page-title.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import defaultSettings from '@/settings'
|
||||
|
||||
const title = defaultSettings.title || 'Vue Admin Template'
|
||||
|
||||
export default function getPageTitle(pageTitle) {
|
||||
if (pageTitle) {
|
||||
return `${pageTitle} - ${title}`
|
||||
}
|
||||
return `${title}`
|
||||
}
|
135
sop-admin/sop-admin-frontend/src/utils/global.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
注册全局方法
|
||||
*/
|
||||
import Vue from 'vue'
|
||||
import axios from 'axios'
|
||||
import { getToken, removeToken } from './auth'
|
||||
|
||||
// 创建axios实例
|
||||
const client = axios.create({
|
||||
baseURL: process.env.VUE_APP_BASE_API + '/api', // api 的 base_url
|
||||
timeout: 60000 // 请求超时时间,60秒
|
||||
})
|
||||
|
||||
Object.assign(Vue.prototype, {
|
||||
/**
|
||||
* 请求接口
|
||||
* @param uri uri,如:goods.get,goods.get/1.0
|
||||
* @param data 请求数据
|
||||
* @param callback 成功时回调
|
||||
* @param errorCallback 错误时回调
|
||||
*/
|
||||
post: function(uri, data, callback, errorCallback) {
|
||||
const that = this
|
||||
const paramStr = JSON.stringify(data)
|
||||
if (!uri.endsWith('/')) {
|
||||
uri = uri + '/'
|
||||
}
|
||||
if (!uri.startsWith('/')) {
|
||||
uri = '/' + uri
|
||||
}
|
||||
client.post(uri, {
|
||||
data: encodeURIComponent(paramStr),
|
||||
access_token: getToken()
|
||||
}).then(function(response) {
|
||||
const resp = response.data
|
||||
const code = resp.code
|
||||
if (!code || code === '-9') {
|
||||
that.$message.error(resp.msg || '系统错误')
|
||||
return
|
||||
}
|
||||
if (code === '-100' || code === '18' || code === '21') { // 未登录
|
||||
that.logout()
|
||||
return
|
||||
}
|
||||
if (code === '0') { // 成功
|
||||
callback && callback.call(that, resp)
|
||||
} else {
|
||||
that.$message.error(resp.msg)
|
||||
}
|
||||
}).catch(function(error) {
|
||||
console.error('err' + error) // for debug
|
||||
errorCallback && errorCallback(error)
|
||||
that.$message.error(error.message)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* tip,使用方式:this.tip('操作成功'),this.tip('错误', 'error')
|
||||
* @param msg 内容
|
||||
* @param type success / info / warning / error
|
||||
* @param stay 停留几秒,默认3秒
|
||||
*/
|
||||
tip: function(msg, type, stay) {
|
||||
stay = parseInt(stay) || 3
|
||||
this.$message({
|
||||
message: msg,
|
||||
type: type || 'success',
|
||||
duration: stay * 1000
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 提醒框
|
||||
* @param msg 消息
|
||||
* @param okHandler 成功回调
|
||||
* @param cancelHandler
|
||||
*/
|
||||
confirm: function(msg, okHandler, cancelHandler) {
|
||||
const that = this
|
||||
this.$confirm(msg, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
okHandler.call(that, done)
|
||||
} else if (action === 'cancel') {
|
||||
if (cancelHandler) {
|
||||
cancelHandler.call(that, done)
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
}
|
||||
}).catch(function() {})
|
||||
},
|
||||
/**
|
||||
* 文件必须放在public下面
|
||||
* @param path 相对于public文件夹路径,如文件在public/static/sign.md,填:static/sign.md
|
||||
* @param callback 回调函数,函数参数是文件内容
|
||||
*/
|
||||
getFile: function(path, callback) {
|
||||
axios.get(path)
|
||||
.then(function(response) {
|
||||
callback.call(this, response.data)
|
||||
})
|
||||
},
|
||||
downloadText(filename, text) {
|
||||
const element = document.createElement('a')
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
|
||||
element.setAttribute('download', filename)
|
||||
|
||||
element.style.display = 'none'
|
||||
document.body.appendChild(element)
|
||||
|
||||
element.click()
|
||||
|
||||
document.body.removeChild(element);
|
||||
},
|
||||
/**
|
||||
* 重置表单
|
||||
* @param formName 表单元素的ref
|
||||
*/
|
||||
resetForm(formName) {
|
||||
const frm = this.$refs[formName]
|
||||
frm && frm.resetFields()
|
||||
},
|
||||
logout: function() {
|
||||
removeToken()
|
||||
const fullPath = this.$route.fullPath
|
||||
if (fullPath.indexOf('login?redirect') === -1) {
|
||||
this.$router.push({ path: `/login?redirect=${fullPath}` })
|
||||
}
|
||||
}
|
||||
})
|
110
sop-admin/sop-admin-frontend/src/utils/index.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Created by PanJiaChen on 16/11/18.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse the time to string
|
||||
* @param {(Object|string|number)} time
|
||||
* @param {string} cFormat
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parseTime(time, cFormat) {
|
||||
if (arguments.length === 0) {
|
||||
return null
|
||||
}
|
||||
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
|
||||
let date
|
||||
if (typeof time === 'object') {
|
||||
date = time
|
||||
} else {
|
||||
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
|
||||
time = parseInt(time)
|
||||
}
|
||||
if ((typeof time === 'number') && (time.toString().length === 10)) {
|
||||
time = time * 1000
|
||||
}
|
||||
date = new Date(time)
|
||||
}
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay()
|
||||
}
|
||||
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||
let value = formatObj[key]
|
||||
// Note: getDay() returns 0 on Sunday
|
||||
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = '0' + value
|
||||
}
|
||||
return value || 0
|
||||
})
|
||||
return time_str
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} time
|
||||
* @param {string} option
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatTime(time, option) {
|
||||
if (('' + time).length === 10) {
|
||||
time = parseInt(time) * 1000
|
||||
} else {
|
||||
time = +time
|
||||
}
|
||||
const d = new Date(time)
|
||||
const now = Date.now()
|
||||
|
||||
const diff = (now - d) / 1000
|
||||
|
||||
if (diff < 30) {
|
||||
return '刚刚'
|
||||
} else if (diff < 3600) {
|
||||
// less 1 hour
|
||||
return Math.ceil(diff / 60) + '分钟前'
|
||||
} else if (diff < 3600 * 24) {
|
||||
return Math.ceil(diff / 3600) + '小时前'
|
||||
} else if (diff < 3600 * 24 * 2) {
|
||||
return '1天前'
|
||||
}
|
||||
if (option) {
|
||||
return parseTime(time, option)
|
||||
} else {
|
||||
return (
|
||||
d.getMonth() +
|
||||
1 +
|
||||
'月' +
|
||||
d.getDate() +
|
||||
'日' +
|
||||
d.getHours() +
|
||||
'时' +
|
||||
d.getMinutes() +
|
||||
'分'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function param2Obj(url) {
|
||||
const search = url.split('?')[1]
|
||||
if (!search) {
|
||||
return {}
|
||||
}
|
||||
return JSON.parse(
|
||||
'{"' +
|
||||
decodeURIComponent(search)
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/&/g, '","')
|
||||
.replace(/=/g, '":"')
|
||||
.replace(/\+/g, ' ') +
|
||||
'"}'
|
||||
)
|
||||
}
|
85
sop-admin/sop-admin-frontend/src/utils/request.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import axios from 'axios'
|
||||
import { MessageBox, Message } from 'element-ui'
|
||||
import store from '@/store'
|
||||
import { getToken } from '@/utils/auth'
|
||||
|
||||
// create an axios instance
|
||||
const service = axios.create({
|
||||
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
|
||||
withCredentials: true, // send cookies when cross-domain requests
|
||||
timeout: 5000 // request timeout
|
||||
})
|
||||
|
||||
// request interceptor
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// do something before request is sent
|
||||
|
||||
if (store.getters.token) {
|
||||
// let each request carry token
|
||||
// ['X-Token'] is a custom headers key
|
||||
// please modify it according to the actual situation
|
||||
config.headers['X-Token'] = getToken()
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
// do something with request error
|
||||
console.log(error) // for debug
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// response interceptor
|
||||
service.interceptors.response.use(
|
||||
/**
|
||||
* If you want to get http information such as headers or status
|
||||
* Please return response => response
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determine the request status by custom code
|
||||
* Here is just an example
|
||||
* You can also judge the status by HTTP Status Code
|
||||
*/
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// if the custom code is not 20000, it is judged as an error.
|
||||
if (res.code !== 20000) {
|
||||
Message({
|
||||
message: res.message || 'error',
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
|
||||
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
|
||||
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
|
||||
// to re-login
|
||||
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
|
||||
confirmButtonText: 'Re-Login',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
store.dispatch('user/resetToken').then(() => {
|
||||
location.reload()
|
||||
})
|
||||
})
|
||||
}
|
||||
return Promise.reject(res.message || 'error')
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log('err' + error) // for debug
|
||||
Message({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
20
sop-admin/sop-admin-frontend/src/utils/validate.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Created by PanJiaChen on 16/11/18.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validUsername(str) {
|
||||
const valid_map = ['admin', 'editor']
|
||||
return valid_map.indexOf(str.trim()) >= 0
|
||||
}
|
228
sop-admin/sop-admin-frontend/src/views/404.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="wscn-http404-container">
|
||||
<div class="wscn-http404">
|
||||
<div class="pic-404">
|
||||
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
|
||||
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||
</div>
|
||||
<div class="bullshit">
|
||||
<div class="bullshit__oops">OOPS!</div>
|
||||
<div class="bullshit__info">All rights reserved
|
||||
<a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
|
||||
</div>
|
||||
<div class="bullshit__headline">{{ message }}</div>
|
||||
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
|
||||
<a href="" class="bullshit__return-home">Back to home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Page404',
|
||||
computed: {
|
||||
message() {
|
||||
return 'The webmaster said that you can not enter this page...'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wscn-http404-container{
|
||||
transform: translate(-50%,-50%);
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
}
|
||||
.wscn-http404 {
|
||||
position: relative;
|
||||
width: 1200px;
|
||||
padding: 0 50px;
|
||||
overflow: hidden;
|
||||
.pic-404 {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 600px;
|
||||
overflow: hidden;
|
||||
&__parent {
|
||||
width: 100%;
|
||||
}
|
||||
&__child {
|
||||
position: absolute;
|
||||
&.left {
|
||||
width: 80px;
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
animation-name: cloudLeft;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
&.mid {
|
||||
width: 46px;
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
animation-name: cloudMid;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
&.right {
|
||||
width: 62px;
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
animation-name: cloudRight;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
@keyframes cloudLeft {
|
||||
0% {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 33px;
|
||||
left: 188px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 81px;
|
||||
left: 92px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 97px;
|
||||
left: 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudMid {
|
||||
0% {
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 40px;
|
||||
left: 360px;
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
top: 130px;
|
||||
left: 180px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 160px;
|
||||
left: 120px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudRight {
|
||||
0% {
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 120px;
|
||||
left: 460px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 180px;
|
||||
left: 340px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 200px;
|
||||
left: 300px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.bullshit {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
padding: 30px 0;
|
||||
overflow: hidden;
|
||||
&__oops {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
color: #1482f0;
|
||||
opacity: 0;
|
||||
margin-bottom: 20px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__headline {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
margin-bottom: 10px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__info {
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
color: grey;
|
||||
opacity: 0;
|
||||
margin-bottom: 30px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__return-home {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 110px;
|
||||
height: 36px;
|
||||
background: #1482f0;
|
||||
border-radius: 100px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
opacity: 0;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
26
sop-admin/sop-admin-frontend/src/views/dashboard/index.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-text">欢迎使用SOP Admin</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
created() {
|
||||
this.post('admin.userinfo.get', {}, function(resp) {
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
&-container {
|
||||
margin: 30px;
|
||||
}
|
||||
&-text {
|
||||
font-size: 18px;
|
||||
line-height: 46px;
|
||||
}
|
||||
}
|
||||
</style>
|
344
sop-admin/sop-admin-frontend/src/views/isv/index.vue
Normal file
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
|
||||
<el-form-item label="AppId">
|
||||
<el-input v-model="searchFormData.appKey" :clearable="true" placeholder="AppId" style="width: 250px;" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="onSearchTable">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" size="mini" icon="el-icon-plus" style="margin-bottom: 10px;" @click="onAdd">新增ISV</el-button>
|
||||
<el-table
|
||||
:data="pageInfo.list"
|
||||
border
|
||||
fit
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column
|
||||
prop="appKey"
|
||||
label="AppId"
|
||||
width="250"
|
||||
/>
|
||||
<el-table-column
|
||||
prop=""
|
||||
label="秘钥"
|
||||
width="80"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="onShowKeys(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="roleList"
|
||||
label="角色"
|
||||
:show-overflow-tooltip="true"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-html="roleRender(scope.row)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="userId"
|
||||
label="注册用户"
|
||||
width="100"
|
||||
>
|
||||
<template slot="header">
|
||||
注册用户
|
||||
<el-tooltip content="注册用户自行管理秘钥" placement="top">
|
||||
<i class="el-icon-question" style="cursor: pointer"></i>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.userId" style="font-weight: bold;">是</span>
|
||||
<span v-else>否</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="状态"
|
||||
width="80"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.status === 1" style="color:#67C23A">启用</span>
|
||||
<span v-if="scope.row.status === 2" style="color:#F56C6C">禁用</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="gmtCreate"
|
||||
label="添加时间"
|
||||
width="160"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="200"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button>
|
||||
<el-button v-if="!scope.row.userId" type="text" size="mini" @click="onKeysUpdate(scope.row)">秘钥管理</el-button>
|
||||
<el-button v-if="!scope.row.userId" type="text" size="mini" @click="onExportKeys(scope.row)">导出秘钥</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
style="margin-top: 5px"
|
||||
:current-page="searchFormData.pageIndex"
|
||||
:page-size="searchFormData.pageSize"
|
||||
:page-sizes="[5, 10, 20, 40]"
|
||||
:total="pageInfo.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onPageIndexChange"
|
||||
/>
|
||||
<!-- dialog -->
|
||||
<el-dialog
|
||||
:title="isvDialogTitle"
|
||||
:visible.sync="isvDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
@close="onIsvDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="isvForm"
|
||||
:rules="rulesIsvForm"
|
||||
:model="isvDialogFormData"
|
||||
label-width="120px"
|
||||
size="mini"
|
||||
>
|
||||
<el-form-item label="appId">
|
||||
<span v-if="isvDialogFormData.id === 0" style="color: gray;">(系统自动生成)</span>
|
||||
<span v-else>{{ isvDialogFormData.appKey }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-checkbox-group v-model="isvDialogFormData.roleCode">
|
||||
<el-checkbox v-for="item in roles" :key="item.roleCode" :label="item.roleCode">{{ item.description }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="isvDialogFormData.remark" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="isvDialogFormData.status">
|
||||
<el-radio :label="1" name="status">启用</el-radio>
|
||||
<el-radio :label="2" name="status">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="isvDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :disabled="isSaveButtonDisabled" @click="onIsvDialogSave">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!--view keys dialog-->
|
||||
<el-dialog
|
||||
title="秘钥信息"
|
||||
:visible.sync="isvKeysDialogVisible"
|
||||
@close="resetForm('isvKeysFrom')"
|
||||
>
|
||||
<el-form
|
||||
ref="isvKeysFrom"
|
||||
:model="isvKeysFormData"
|
||||
label-width="160px"
|
||||
size="mini"
|
||||
class="key-view"
|
||||
>
|
||||
<el-form-item label="appId">
|
||||
<span>{{ isvKeysFormData.appKey }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="showKeys()" label="秘钥格式">
|
||||
<span v-if="isvKeysFormData.keyFormat === 1">PKCS8(JAVA适用)</span>
|
||||
<span v-if="isvKeysFormData.keyFormat === 2">PKCS1(非JAVA适用)</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="isvKeysFormData.signType === 2" label="secret">
|
||||
<span>{{ isvKeysFormData.secret }}</span>
|
||||
</el-form-item>
|
||||
<el-tabs v-show="showKeys()" v-model="activeName" type="card" class="keyTabs">
|
||||
<el-tab-pane label="ISV公私钥" name="first">
|
||||
<el-form-item label="ISV公钥">
|
||||
<el-input v-model="isvKeysFormData.publicKeyIsv" type="textarea" placeholder="未上传" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item v-show="isvKeysFormData.userId === 0" label="ISV私钥">
|
||||
<el-input v-model="isvKeysFormData.privateKeyIsv" type="textarea" readonly />
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="平台公私钥" name="second">
|
||||
<el-form-item label="平台公钥">
|
||||
<el-input v-model="isvKeysFormData.publicKeyPlatform" type="textarea" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item prop="privateKeyPlatform" label="平台私钥">
|
||||
<el-input v-model="isvKeysFormData.privateKeyPlatform" type="textarea" readonly />
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="isvKeysDialogVisible = false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.gen-key {margin-bottom: 0px !important;}
|
||||
fieldset {border: 1px solid #ccc; color: gray;margin-left: 40px;margin-bottom: 20px;}
|
||||
fieldset label {width: 110px !important;}
|
||||
fieldset .el-form-item__content {margin-left: 110px !important;}
|
||||
.key-view .el-form-item {margin-bottom: 10px !important;}
|
||||
.keyTabs .el-tabs__header{margin-left: 70px;}
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
searchFormData: {
|
||||
appKey: '',
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
pageInfo: {
|
||||
list: [],
|
||||
total: 0
|
||||
},
|
||||
roles: [],
|
||||
// dialog
|
||||
isvDialogVisible: false,
|
||||
isvDialogTitle: '新增ISV',
|
||||
isvDialogFormData: {
|
||||
id: 0,
|
||||
status: 1,
|
||||
remark: '',
|
||||
roleCode: []
|
||||
},
|
||||
rulesIsvForm: {
|
||||
remark: [
|
||||
{ min: 0, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
activeName: 'first',
|
||||
isSaveButtonDisabled: false,
|
||||
isvKeysDialogVisible: false,
|
||||
isvKeysFormData: {
|
||||
appKey: '',
|
||||
secret: '',
|
||||
publicKeyIsv: '',
|
||||
privateKeyIsv: '',
|
||||
publicKeyPlatform: '',
|
||||
privateKeyPlatform: '',
|
||||
signType: '',
|
||||
userId: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTable()
|
||||
this.loadRouteRole()
|
||||
},
|
||||
methods: {
|
||||
loadTable() {
|
||||
this.post('isv.info.page', this.searchFormData, function(resp) {
|
||||
this.pageInfo = resp.data
|
||||
})
|
||||
},
|
||||
loadRouteRole: function() {
|
||||
if (this.roles.length === 0) {
|
||||
this.post('role.listall', {}, function(resp) {
|
||||
this.roles = resp.data
|
||||
})
|
||||
}
|
||||
},
|
||||
onShowKeys: function(row) {
|
||||
this.post('isv.keys.get', { appKey: row.appKey }, function(resp) {
|
||||
this.isvKeysDialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
Object.assign(this.isvKeysFormData, resp.data)
|
||||
this.isvKeysFormData.userId = row.userId
|
||||
})
|
||||
})
|
||||
},
|
||||
onSearchTable: function() {
|
||||
this.loadTable()
|
||||
},
|
||||
onTableUpdate: function(row) {
|
||||
this.isvDialogTitle = '修改ISV'
|
||||
this.isvDialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.post('isv.info.get', { id: row.id }, function(resp) {
|
||||
const isvInfo = resp.data
|
||||
const roleList = isvInfo.roleList
|
||||
const roleCode = []
|
||||
for (let i = 0; i < roleList.length; i++) {
|
||||
roleCode.push(roleList[i].roleCode)
|
||||
}
|
||||
isvInfo.roleCode = roleCode
|
||||
Object.assign(this.isvDialogFormData, isvInfo)
|
||||
})
|
||||
})
|
||||
},
|
||||
onKeysUpdate: function(row) {
|
||||
this.$router.push({ path: `keys?appKey=${row.appKey}` })
|
||||
},
|
||||
onExportKeys: function(row) {
|
||||
this.post('isv.keys.get', { appKey: row.appKey }, function(resp) {
|
||||
const data = resp.data
|
||||
const appId = data.appKey
|
||||
const privateKeyIsv = data.privateKeyIsv
|
||||
const publicKeyPlatform = data.publicKeyPlatform
|
||||
let content = `AppId:${appId}\n\n开发者私钥:\n${privateKeyIsv}\n\n`
|
||||
if (publicKeyPlatform) {
|
||||
content = content + `平台公钥:\n${publicKeyPlatform}`
|
||||
}
|
||||
const filename = `${appId}.txt`
|
||||
this.downloadText(filename, content)
|
||||
})
|
||||
},
|
||||
onSizeChange: function(size) {
|
||||
this.searchFormData.pageSize = size
|
||||
this.loadTable()
|
||||
},
|
||||
onPageIndexChange: function(pageIndex) {
|
||||
this.searchFormData.pageIndex = pageIndex
|
||||
this.loadTable()
|
||||
},
|
||||
onAdd: function() {
|
||||
this.isvDialogTitle = '新增ISV'
|
||||
this.isvDialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.isvDialogFormData.id = 0
|
||||
})
|
||||
},
|
||||
onIsvDialogSave: function() {
|
||||
this.$refs.isvForm.validate((valid) => {
|
||||
if (valid) {
|
||||
this.isSaveButtonDisabled = true
|
||||
const uri = this.isvDialogFormData.id === 0 ? 'isv.info.add' : 'isv.info.update'
|
||||
this.post(uri, this.isvDialogFormData, function() {
|
||||
this.isvDialogVisible = false
|
||||
this.loadTable()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onIsvDialogClose: function() {
|
||||
this.resetForm('isvForm')
|
||||
this.isSaveButtonDisabled = false
|
||||
this.isvDialogFormData.status = 1
|
||||
this.isvDialogFormData.roleCode = []
|
||||
},
|
||||
roleRender: function(row) {
|
||||
const html = []
|
||||
const roleList = row.roleList
|
||||
for (let i = 0; i < roleList.length; i++) {
|
||||
html.push(roleList[i].description)
|
||||
}
|
||||
return html.join(', ')
|
||||
},
|
||||
showKeys: function() {
|
||||
return this.isvKeysFormData.signType === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
168
sop-admin/sop-admin-frontend/src/views/isv/keys.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-button class="el-icon-back" type="text" @click="onBack">返回</el-button>
|
||||
<el-form
|
||||
ref="isvKeysForm"
|
||||
:rules="rulesIsvKeysForm"
|
||||
:model="isvKeysFormData"
|
||||
label-width="160px"
|
||||
size="mini"
|
||||
style="width: 700px;"
|
||||
>
|
||||
<el-form-item label="">
|
||||
<el-alert
|
||||
title="带 ★ 的分配给开发者"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="selfLabel('appId')">
|
||||
<div>{{ isvKeysFormData.appKey }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="showKeys()" label="秘钥格式">
|
||||
<el-radio-group v-model="isvKeysFormData.keyFormat">
|
||||
<el-radio :label="1" name="keyFormat">PKCS8(JAVA适用)</el-radio>
|
||||
<el-radio :label="2" name="keyFormat">PKCS1(非JAVA适用)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="isvKeysFormData.signType === 2" prop="secret" :label="selfLabel('secret')">
|
||||
<el-input v-model="isvKeysFormData.secret" /> <el-button type="text" @click="onGenSecret">重新生成</el-button>
|
||||
</el-form-item>
|
||||
<el-tabs v-show="showKeys()" v-model="activeName" type="card" class="keyTabs">
|
||||
<el-tab-pane label="ISV公私钥" name="first">
|
||||
<el-form-item class="gen-key">
|
||||
<el-button type="text" @click="onGenKeysIsv">重新生成</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item prop="publicKeyIsv" label="ISV公钥">
|
||||
<el-input v-model="isvKeysFormData.publicKeyIsv" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="privateKeyIsv" :label="selfLabel('ISV私钥')">
|
||||
<el-input v-model="isvKeysFormData.privateKeyIsv" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="平台公私钥[可选]" name="second">
|
||||
<el-form-item class="gen-key">
|
||||
<el-button type="text" @click="onGenKeysPlatform">重新生成</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item prop="publicKeyPlatform" label="平台公钥">
|
||||
<el-input v-model="isvKeysFormData.publicKeyPlatform" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="privateKeyPlatform" label="平台私钥">
|
||||
<el-input v-model="isvKeysFormData.privateKeyPlatform" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="onSubmit">保存</el-button>
|
||||
<el-button @click="onBack">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.gen-key {margin-bottom: 0px !important;}
|
||||
fieldset {border: 1px solid #ccc; color: gray;margin-left: 40px;margin-bottom: 20px;}
|
||||
fieldset label {width: 110px !important;}
|
||||
fieldset .el-form-item__content {margin-left: 110px !important;}
|
||||
.keyTabs .el-tabs__header{margin-left: 70px;}
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
const validateSecret = (rule, value, callback) => {
|
||||
if (this.isvKeysFormData.signType === 2) {
|
||||
if (value === '') {
|
||||
callback(new Error('不能为空'))
|
||||
}
|
||||
if (value.length > 200) {
|
||||
callback(new Error('长度不能超过200'))
|
||||
}
|
||||
}
|
||||
callback()
|
||||
}
|
||||
const validatePubPriKey = (rule, value, callback) => {
|
||||
if (this.isvKeysFormData.signType === 1) {
|
||||
if (value === '') {
|
||||
callback(new Error('不能为空'))
|
||||
}
|
||||
}
|
||||
callback()
|
||||
}
|
||||
return {
|
||||
isvKeysFormData: {
|
||||
appKey: '',
|
||||
secret: '',
|
||||
keyFormat: 1,
|
||||
publicKeyIsv: '',
|
||||
privateKeyIsv: '',
|
||||
publicKeyPlatform: '',
|
||||
privateKeyPlatform: '',
|
||||
signType: 1
|
||||
},
|
||||
rulesIsvKeysForm: {
|
||||
secret: [
|
||||
{ validator: validateSecret, trigger: 'blur' }
|
||||
],
|
||||
publicKeyIsv: [
|
||||
{ validator: validatePubPriKey, trigger: 'blur' }
|
||||
],
|
||||
privateKeyIsv: [
|
||||
{ validator: validatePubPriKey, trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
activeName: 'first'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const query = this.$route.query
|
||||
this.isvKeysFormData.appKey = query.appKey
|
||||
this.loadForm()
|
||||
},
|
||||
methods: {
|
||||
loadForm: function() {
|
||||
this.post('isv.keys.get', { appKey: this.isvKeysFormData.appKey }, function(resp) {
|
||||
Object.assign(this.isvKeysFormData, resp.data)
|
||||
})
|
||||
},
|
||||
selfLabel: function(lab) {
|
||||
return '★ ' + lab
|
||||
},
|
||||
onSubmit: function() {
|
||||
this.$refs.isvKeysForm.validate((valid) => {
|
||||
if (valid) {
|
||||
this.post('isv.keys.update', this.isvKeysFormData, function() {
|
||||
this.tip('保存成功')
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onBack: function() {
|
||||
this.$router.push({ path: 'list' })
|
||||
},
|
||||
onGenKeysPlatform: function() {
|
||||
this.post('isv.keys.gen', {}, function(resp) {
|
||||
this.tip('生成公私钥成功')
|
||||
const data = resp.data
|
||||
this.isvKeysFormData.publicKeyPlatform = data.publicKey
|
||||
this.isvKeysFormData.privateKeyPlatform = data.privateKey
|
||||
})
|
||||
},
|
||||
onGenKeysIsv: function() {
|
||||
this.post('isv.keys.gen', { keyFormat: this.isvKeysFormData.keyFormat }, function(resp) {
|
||||
this.tip('生成公私钥成功')
|
||||
const data = resp.data
|
||||
this.isvKeysFormData.publicKeyIsv = data.publicKey
|
||||
this.isvKeysFormData.privateKeyIsv = data.privateKey
|
||||
})
|
||||
},
|
||||
onGenSecret: function() {
|
||||
this.post('isv.secret.gen', {}, function(resp) {
|
||||
this.isvKeysFormData.secret = resp.data
|
||||
})
|
||||
},
|
||||
showKeys: function() {
|
||||
return this.isvKeysFormData.signType === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
173
sop-admin/sop-admin-frontend/src/views/isv/role.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
|
||||
<el-form-item label="角色码">
|
||||
<el-input v-model="searchFormData.roleCode" :clearable="true" placeholder="输入角色码" style="width: 250px;" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="loadTable">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" size="mini" icon="el-icon-plus" style="margin-bottom: 10px;" @click="onAdd">新增角色</el-button>
|
||||
<el-table
|
||||
:data="pageInfo.rows"
|
||||
border
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column
|
||||
prop="roleCode"
|
||||
label="角色码"
|
||||
width="200"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="description"
|
||||
label="角色描述"
|
||||
width="200"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="gmtCreate"
|
||||
label="添加时间"
|
||||
width="160"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="gmtModified"
|
||||
label="修改时间"
|
||||
width="160"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button>
|
||||
<el-button type="text" size="mini" @click="onTableDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
style="margin-top: 5px"
|
||||
:current-page="searchFormData.pageIndex"
|
||||
:page-size="searchFormData.pageSize"
|
||||
:page-sizes="[5, 10, 20, 40]"
|
||||
:total="pageInfo.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onPageIndexChange"
|
||||
/>
|
||||
<!--dialog-->
|
||||
<el-dialog
|
||||
:title="roleDialogTitle"
|
||||
:visible.sync="roleDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
@close="resetForm('roleForm')"
|
||||
>
|
||||
<el-form
|
||||
ref="roleForm"
|
||||
:rules="roleDialogFormRules"
|
||||
:model="roleDialogFormData"
|
||||
label-width="120px"
|
||||
size="mini"
|
||||
>
|
||||
<el-form-item prop="roleCode" label="角色码">
|
||||
<el-input v-show="roleDialogFormData.id === 0" v-model="roleDialogFormData.roleCode" />
|
||||
<span v-show="roleDialogFormData.id > 0">{{ roleDialogFormData.roleCode }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item prop="description" label="角色描述">
|
||||
<el-input v-model="roleDialogFormData.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="roleDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onRoleDialogSave">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
searchFormData: {
|
||||
roleCode: '',
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
pageInfo: {
|
||||
rows: [],
|
||||
total: 0
|
||||
},
|
||||
roleDialogVisible: false,
|
||||
roleDialogTitle: '',
|
||||
roleDialogFormData: {
|
||||
id: 0,
|
||||
roleCode: '',
|
||||
description: ''
|
||||
},
|
||||
roleDialogFormRules: {
|
||||
roleCode: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||
{ min: 1, max: 64, message: '长度在 1 到 64 个字符', trigger: 'blur' }
|
||||
],
|
||||
description: [
|
||||
{ max: 64, message: '不能超过 64 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTable()
|
||||
},
|
||||
methods: {
|
||||
loadTable: function() {
|
||||
this.post('role.page', this.searchFormData, function(resp) {
|
||||
this.pageInfo = resp.data
|
||||
})
|
||||
},
|
||||
onTableUpdate: function(row) {
|
||||
this.roleDialogTitle = '修改角色'
|
||||
this.roleDialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
Object.assign(this.roleDialogFormData, row)
|
||||
})
|
||||
},
|
||||
onTableDelete: function(row) {
|
||||
this.confirm(`确认要删除角色【${row.roleCode}】吗?`, function(done) {
|
||||
const data = {
|
||||
id: row.id
|
||||
}
|
||||
this.post('role.del', data, function() {
|
||||
done()
|
||||
this.tip('删除成功')
|
||||
this.loadTable()
|
||||
})
|
||||
})
|
||||
},
|
||||
onRoleDialogSave: function() {
|
||||
this.$refs.roleForm.validate((valid) => {
|
||||
if (valid) {
|
||||
const uri = this.roleDialogFormData.id ? 'role.update' : 'role.add'
|
||||
this.post(uri, this.roleDialogFormData, function() {
|
||||
this.roleDialogVisible = false
|
||||
this.loadTable()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onSizeChange: function(size) {
|
||||
this.searchFormData.pageSize = size
|
||||
this.loadTable()
|
||||
},
|
||||
onAdd: function() {
|
||||
this.roleDialogTitle = '新增角色'
|
||||
this.roleDialogVisible = true
|
||||
this.roleDialogFormData.id = 0
|
||||
},
|
||||
onPageIndexChange: function(pageIndex) {
|
||||
this.searchFormData.pageIndex = pageIndex
|
||||
this.loadTable()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
248
sop-admin/sop-admin-frontend/src/views/login/index.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
|
||||
|
||||
<div class="title-container">
|
||||
<h3 class="title">SOP Admin</h3>
|
||||
</div>
|
||||
|
||||
<el-form-item prop="username">
|
||||
<span class="svg-container">
|
||||
<svg-icon icon-class="user" />
|
||||
</span>
|
||||
<el-input
|
||||
ref="username"
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
name="username"
|
||||
type="text"
|
||||
tabindex="1"
|
||||
auto-complete="on"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<span class="svg-container">
|
||||
<svg-icon icon-class="password" />
|
||||
</span>
|
||||
<el-input
|
||||
:key="passwordType"
|
||||
ref="password"
|
||||
v-model="loginForm.password"
|
||||
:type="passwordType"
|
||||
placeholder="密码"
|
||||
name="password"
|
||||
tabindex="2"
|
||||
auto-complete="on"
|
||||
@keyup.enter.native="handleLogin"
|
||||
/>
|
||||
<span class="show-pwd" @click="showPwd">
|
||||
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登 录</el-button>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import md5 from 'js-md5'
|
||||
import { setToken } from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
data() {
|
||||
const validateUsername = (rule, value, callback) => {
|
||||
if (value.length === 0) {
|
||||
callback(new Error('请输入用户名'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const validatePassword = (rule, value, callback) => {
|
||||
if (value.length === 0) {
|
||||
callback(new Error('请输入密码'))
|
||||
} else if (value.length < 6) {
|
||||
callback(new Error('请密码长度不得小于6位'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
loginForm: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loginRules: {
|
||||
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
|
||||
password: [{ required: true, trigger: 'blur', validator: validatePassword }]
|
||||
},
|
||||
loading: false,
|
||||
passwordType: 'password',
|
||||
redirect: undefined
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler: function(route) {
|
||||
this.redirect = route.query && route.query.redirect
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showPwd() {
|
||||
if (this.passwordType === 'password') {
|
||||
this.passwordType = ''
|
||||
} else {
|
||||
this.passwordType = 'password'
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.password.focus()
|
||||
})
|
||||
},
|
||||
handleLogin() {
|
||||
this.$refs.loginForm.validate(valid => {
|
||||
// if (valid) {
|
||||
// this.loading = true
|
||||
// this.$store.dispatch('user/login', this.loginForm).then(() => {
|
||||
// this.$router.push({ path: this.redirect || '/' })
|
||||
// this.loading = false
|
||||
// }).catch(() => {
|
||||
// this.loading = false
|
||||
// })
|
||||
// } else {
|
||||
// console.log('error submit!!')
|
||||
// return false
|
||||
// }
|
||||
if (valid) {
|
||||
const data = this.loginForm
|
||||
let pwd = data.password
|
||||
pwd = md5(pwd)
|
||||
const postData = {
|
||||
username: data.username,
|
||||
password: pwd
|
||||
}
|
||||
this.post('nologin.admin.login', postData, function(resp) {
|
||||
setToken(resp.data)
|
||||
this.$router.push({ path: this.redirect || '/' })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 修复input 背景不协调 和光标变色 */
|
||||
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
|
||||
|
||||
$bg:#283443;
|
||||
$light_gray:#fff;
|
||||
$cursor: #fff;
|
||||
|
||||
@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
|
||||
.login-container .el-input input {
|
||||
color: $cursor;
|
||||
}
|
||||
}
|
||||
|
||||
/* reset element-ui css */
|
||||
.login-container {
|
||||
.el-input {
|
||||
display: inline-block;
|
||||
height: 47px;
|
||||
width: 85%;
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0px;
|
||||
padding: 12px 5px 12px 15px;
|
||||
color: $light_gray;
|
||||
height: 47px;
|
||||
caret-color: $cursor;
|
||||
|
||||
&:-webkit-autofill {
|
||||
box-shadow: 0 0 0px 1000px $bg inset !important;
|
||||
-webkit-text-fill-color: $cursor !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
color: #454545;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$bg:#2d3a4b;
|
||||
$dark_gray:#889aa4;
|
||||
$light_gray:#eee;
|
||||
|
||||
.login-container {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
background-color: $bg;
|
||||
overflow: hidden;
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
width: 520px;
|
||||
max-width: 100%;
|
||||
padding: 160px 35px 0;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
margin-bottom: 10px;
|
||||
|
||||
span {
|
||||
&:first-of-type {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
padding: 6px 5px 6px 15px;
|
||||
color: $dark_gray;
|
||||
vertical-align: middle;
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
position: relative;
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
color: $light_gray;
|
||||
margin: 0px auto 40px auto;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.show-pwd {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 7px;
|
||||
font-size: 16px;
|
||||
color: $dark_gray;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
184
sop-admin/sop-admin-frontend/src/views/service/ipBlacklist.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
|
||||
<el-form-item label="IP">
|
||||
<el-input v-model="searchFormData.ip" :clearable="true" placeholder="输入IP" style="width: 250px;" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="loadTable">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" size="mini" icon="el-icon-plus" style="margin-bottom: 10px;" @click="onAdd">新增IP</el-button>
|
||||
<el-table
|
||||
:data="pageInfo.rows"
|
||||
border
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column
|
||||
prop="ip"
|
||||
label="IP"
|
||||
width="200"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
width="300"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="gmtCreate"
|
||||
label="添加时间"
|
||||
width="160"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="gmtModified"
|
||||
label="修改时间"
|
||||
width="160"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button>
|
||||
<el-button type="text" size="mini" @click="onTableDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
style="margin-top: 5px"
|
||||
:current-page="searchFormData.pageIndex"
|
||||
:page-size="searchFormData.pageSize"
|
||||
:page-sizes="[5, 10, 20, 40]"
|
||||
:total="pageInfo.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onPageIndexChange"
|
||||
/>
|
||||
<!--dialog-->
|
||||
<el-dialog
|
||||
:title="dialogTitle"
|
||||
:visible.sync="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
@close="resetForm('dialogForm')"
|
||||
>
|
||||
<el-form
|
||||
ref="dialogForm"
|
||||
:rules="dialogFormRules"
|
||||
:model="dialogFormData"
|
||||
label-width="120px"
|
||||
size="mini"
|
||||
>
|
||||
<el-form-item prop="ip" label="IP">
|
||||
<el-input v-show="dialogFormData.id === 0" v-model="dialogFormData.ip" />
|
||||
<span v-show="dialogFormData.id > 0">{{ dialogFormData.ip }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model="dialogFormData.remark" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onDialogSave">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
const regexIP = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
|
||||
const ipValidator = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请输入IP'))
|
||||
} else {
|
||||
if (!regexIP.test(value)) {
|
||||
callback(new Error('IP格式不正确'))
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
searchFormData: {
|
||||
ip: '',
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
pageInfo: {
|
||||
rows: [],
|
||||
total: 0
|
||||
},
|
||||
dialogVisible: false,
|
||||
dialogTitle: '',
|
||||
dialogFormData: {
|
||||
id: 0,
|
||||
ip: '',
|
||||
remark: ''
|
||||
},
|
||||
dialogFormRules: {
|
||||
ip: [
|
||||
{ validator: ipValidator, trigger: 'blur' },
|
||||
{ min: 1, max: 64, message: '长度在 1 到 64 个字符', trigger: 'blur' }
|
||||
],
|
||||
remark: [
|
||||
{ max: 100, message: '不能超过 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTable()
|
||||
},
|
||||
methods: {
|
||||
loadTable: function() {
|
||||
this.post('ip.blacklist.page', this.searchFormData, function(resp) {
|
||||
this.pageInfo = resp.data
|
||||
})
|
||||
},
|
||||
onTableUpdate: function(row) {
|
||||
this.dialogTitle = '修改IP'
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
Object.assign(this.dialogFormData, row)
|
||||
})
|
||||
},
|
||||
onTableDelete: function(row) {
|
||||
this.confirm(`确认要移除IP【${row.ip}】吗?`, function(done) {
|
||||
const data = {
|
||||
id: row.id
|
||||
}
|
||||
this.post('ip.blacklist.del', data, function() {
|
||||
done()
|
||||
this.tip('删除成功')
|
||||
this.loadTable()
|
||||
})
|
||||
})
|
||||
},
|
||||
onDialogSave: function() {
|
||||
this.$refs.dialogForm.validate((valid) => {
|
||||
if (valid) {
|
||||
const uri = this.dialogFormData.id ? 'ip.blacklist.update' : 'ip.blacklist.add'
|
||||
this.post(uri, this.dialogFormData, function() {
|
||||
this.dialogVisible = false
|
||||
this.loadTable()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onSizeChange: function(size) {
|
||||
this.searchFormData.pageSize = size
|
||||
this.loadTable()
|
||||
},
|
||||
onAdd: function() {
|
||||
this.dialogTitle = '新增IP'
|
||||
this.dialogVisible = true
|
||||
this.dialogFormData.id = 0
|
||||
},
|
||||
onPageIndexChange: function(pageIndex) {
|
||||
this.searchFormData.pageIndex = pageIndex
|
||||
this.loadTable()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
516
sop-admin/sop-admin-frontend/src/views/service/limit.vue
Normal file
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div v-if="tabsData.length === 0">
|
||||
无服务
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tabs v-model="tabsActive" type="card" @tab-click="selectTab">
|
||||
<el-tab-pane v-for="tabName in tabsData" :key="tabName" :label="tabName" :name="tabName" />
|
||||
</el-tabs>
|
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
|
||||
<el-form-item label="路由ID">
|
||||
<el-input v-model="searchFormData.routeId" placeholder="接口名,支持模糊查询" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="AppId">
|
||||
<el-input v-model="searchFormData.appKey" placeholder="AppId,支持模糊查询" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="IP">
|
||||
<el-input v-model="searchFormData.limitIp" placeholder="ip,支持模糊查询" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="onSearchTable">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" size="mini" icon="el-icon-plus" style="margin-bottom: 10px;" @click="onAdd">新增限流</el-button>
|
||||
<el-table
|
||||
:data="pageInfo.list"
|
||||
border
|
||||
>
|
||||
<el-table-column
|
||||
prop="limitKey"
|
||||
label="限流维度"
|
||||
width="400"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<div v-html="limitRender(scope.row)"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="limitType"
|
||||
label="限流策略"
|
||||
width="120"
|
||||
>
|
||||
<template slot="header" slot-scope>
|
||||
限流策略
|
||||
<el-popover
|
||||
ref="popover"
|
||||
placement="top"
|
||||
title="限流策略"
|
||||
width="500"
|
||||
trigger="hover">
|
||||
<div>
|
||||
<p>窗口策略:每秒处理固定数量的请求,超出请求数量返回错误信息。</p>
|
||||
<p>令牌桶策略:每秒放置固定数量的令牌数,每个请求进来后先去拿令牌,拿到了令牌才能继续,拿不到则等候令牌重新生成了再拿。</p>
|
||||
</div>
|
||||
</el-popover>
|
||||
<i v-popover:popover class="el-icon-question" style="cursor: pointer"></i>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.limitType === 1">窗口策略</span>
|
||||
<span v-if="scope.row.limitType === 2">令牌桶策略</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="info"
|
||||
label="限流信息"
|
||||
width="250"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-html="infoRender(scope.row)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="limitStatus"
|
||||
label="状态"
|
||||
width="80"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.limitStatus === 1" style="color:#67C23A">已开启</span>
|
||||
<span v-if="scope.row.limitStatus === 0" style="color:#909399">已关闭</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="orderIndex"
|
||||
label="排序"
|
||||
width="80"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
width="150"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="gmtCreate"
|
||||
label="创建时间"
|
||||
width="160"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="gmtModified"
|
||||
label="修改时间"
|
||||
width="160"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
fixed="right"
|
||||
width="80"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
style="margin-top: 5px"
|
||||
:current-page="searchFormData.pageIndex"
|
||||
:page-size="searchFormData.pageSize"
|
||||
:page-sizes="[5, 10, 20, 40]"
|
||||
:total="pageInfo.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onPageIndexChange"
|
||||
/>
|
||||
</div>
|
||||
<!-- dialog -->
|
||||
<el-dialog
|
||||
:title="dlgTitle"
|
||||
:visible.sync="limitDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
@close="onLimitDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="limitDialogForm"
|
||||
:model="limitDialogFormData"
|
||||
:rules="rulesLimit"
|
||||
label-width="150px"
|
||||
size="mini"
|
||||
>
|
||||
<el-form-item label="限流维度" prop="typeKey">
|
||||
<el-checkbox-group v-model="limitDialogFormData.typeKey">
|
||||
<el-checkbox v-model="limitDialogFormData.typeKey[0]" :label="1" name="typeKey">路由ID</el-checkbox>
|
||||
<el-checkbox v-model="limitDialogFormData.typeKey[1]" :label="2" name="typeKey">AppId</el-checkbox>
|
||||
<el-checkbox v-model="limitDialogFormData.typeKey[2]" :label="3" name="typeKey">IP</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="checkTypeKey(1)" prop="routeId" label="路由ID" :rules="checkTypeKey(1) ? rulesLimit.routeId : []">
|
||||
<el-select v-model="limitDialogFormData.routeId" filterable placeholder="可筛选" style="width: 300px;">
|
||||
<el-option
|
||||
v-for="item in routeList"
|
||||
:key="item.id"
|
||||
:label="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
<span style="float: left">{{ item.name }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.version }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="checkTypeKey(2)" prop="appKey" label="AppId" :rules="checkTypeKey(2) ? rulesLimit.appKey : []">
|
||||
<el-input v-model="limitDialogFormData.appKey" placeholder="需要限流的AppId" />
|
||||
</el-form-item>
|
||||
<el-form-item v-show="checkTypeKey(3)" label="限流IP" prop="limitIp" :rules="checkTypeKey(3) ? rulesLimit.ip : []">
|
||||
<el-input v-model="limitDialogFormData.limitIp" type="textarea" :rows="2" placeholder="多个用英文逗号隔开" />
|
||||
</el-form-item>
|
||||
<el-form-item label="限流策略">
|
||||
<el-radio-group v-model="limitDialogFormData.limitType">
|
||||
<el-radio :label="1">窗口策略</el-radio>
|
||||
<el-radio :label="2">令牌桶策略</el-radio>
|
||||
</el-radio-group>
|
||||
<el-popover
|
||||
ref="popover"
|
||||
placement="top"
|
||||
title="限流策略"
|
||||
width="500"
|
||||
trigger="hover">
|
||||
<div>
|
||||
<p>窗口策略:每秒处理固定数量的请求,超出请求数量返回错误信息。</p>
|
||||
<p>令牌桶策略:每秒放置固定数量的令牌数,每个请求进来后先去拿令牌,拿到了令牌才能继续,拿不到则等候令牌重新生成了再拿。</p>
|
||||
</div>
|
||||
</el-popover>
|
||||
<i v-popover:popover class="el-icon-question" style="cursor: pointer"></i>
|
||||
</el-form-item>
|
||||
<el-form-item label="开启状态">
|
||||
<el-switch
|
||||
v-model="limitDialogFormData.limitStatus"
|
||||
active-color="#13ce66"
|
||||
inactive-color="#ff4949"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
>
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="orderIndex">
|
||||
<el-input-number v-model="limitDialogFormData.orderIndex" controls-position="right" :min="0" />
|
||||
<span class="tip" style="margin-left: 10px">值小优先执行</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="isWindowType()" label="请求数" prop="execCountPerSecond" :rules="isWindowType() ? rulesLimit.execCountPerSecond : []">
|
||||
每 <el-input-number v-model="limitDialogFormData.durationSeconds" controls-position="right" :min="1" /> 秒可处理
|
||||
<el-input-number v-model="limitDialogFormData.execCountPerSecond" controls-position="right" :min="1" /> 个请求
|
||||
</el-form-item>
|
||||
<el-form-item v-show="isTokenType()" label="令牌桶容量" prop="tokenBucketCount" :rules="isTokenType() ? rulesLimit.tokenBucketCount : []">
|
||||
<el-input-number v-model="limitDialogFormData.tokenBucketCount" controls-position="right" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="limitDialogFormData.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="limitDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onLimitDialogSave">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.custom-tree-node {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.el-input.is-disabled .el-input__inner {color: #909399;}
|
||||
.el-radio__input.is-disabled+span.el-radio__label {color: #909399;}
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tabsData: [],
|
||||
tabsActive: '',
|
||||
serviceTextLimitSize: 20,
|
||||
filterText: '',
|
||||
treeData: [],
|
||||
tableData: [],
|
||||
serviceId: '',
|
||||
searchFormData: {
|
||||
pageIndex: 1,
|
||||
pageSize: 5
|
||||
},
|
||||
pageInfo: {
|
||||
list: [],
|
||||
total: 0
|
||||
},
|
||||
routeList: [],
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'label'
|
||||
},
|
||||
// dialog
|
||||
dlgTitle: '设置限流',
|
||||
limitDialogVisible: false,
|
||||
limitDialogFormData: {
|
||||
id: 0,
|
||||
routeId: '',
|
||||
appKey: '',
|
||||
limitIp: '',
|
||||
limitKey: '',
|
||||
execCountPerSecond: 5,
|
||||
durationSeconds: 1,
|
||||
limitCode: '',
|
||||
limitMsg: '',
|
||||
tokenBucketCount: 5,
|
||||
limitStatus: 0, // 0: 停用,1:启用
|
||||
limitType: 1,
|
||||
orderIndex: 0,
|
||||
remark: '',
|
||||
typeKey: []
|
||||
},
|
||||
rulesLimit: {
|
||||
typeKey: [
|
||||
{ type: 'array', required: true, message: '请至少选择一个', trigger: 'change' }
|
||||
],
|
||||
routeId: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
appKey: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
ip: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||
{ min: 1, max: 500, message: '长度在 1 到 500 个字符', trigger: 'blur' }
|
||||
],
|
||||
// window
|
||||
execCountPerSecond: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
],
|
||||
// token
|
||||
tokenBucketCount: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
],
|
||||
orderIndex: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
],
|
||||
remark: [
|
||||
{ max: 128, message: '长度不能超过128字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filterText(val) {
|
||||
this.$refs.tree2.filter(val)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTabs()
|
||||
},
|
||||
methods: {
|
||||
loadTabs() {
|
||||
this.post('registry.service.list', {}, function(resp) {
|
||||
this.tabsData = resp.data
|
||||
this.$nextTick(() => {
|
||||
if (this.tabsData.length > 0) {
|
||||
this.tabsActive = this.tabsData[0]
|
||||
this.loadLimitData()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
// 加载树
|
||||
loadTree: function() {
|
||||
this.post('registry.service.list', {}, function(resp) {
|
||||
const respData = resp.data
|
||||
this.treeData = this.convertToTreeData(respData, 0)
|
||||
})
|
||||
},
|
||||
// 树搜索
|
||||
filterNode(value, data) {
|
||||
if (!value) return true
|
||||
return data.label.indexOf(value) !== -1
|
||||
},
|
||||
// 树点击事件
|
||||
onNodeClick(data, node, tree) {
|
||||
if (data.parentId) {
|
||||
this.serviceId = data.label
|
||||
this.searchFormData.serviceId = this.serviceId
|
||||
this.loadTable()
|
||||
this.loadRouteList(this.serviceId)
|
||||
}
|
||||
},
|
||||
selectTab() {
|
||||
this.loadLimitData()
|
||||
},
|
||||
loadLimitData() {
|
||||
this.serviceId = this.tabsActive
|
||||
this.searchFormData.serviceId = this.serviceId
|
||||
this.loadTable()
|
||||
this.loadRouteList(this.serviceId)
|
||||
},
|
||||
/**
|
||||
* 数组转成树状结构
|
||||
* @param data 数据结构 [{
|
||||
"_parentId": 14,
|
||||
"gmtCreate": "2019-01-15 09:44:38",
|
||||
"gmtUpdate": "2019-01-15 09:44:38",
|
||||
"id": 15,
|
||||
"isShow": 1,
|
||||
"name": "用户注册",
|
||||
"orderIndex": 10000,
|
||||
"parentId": 14
|
||||
},...]
|
||||
* @param pid 初始父节点id,一般是0
|
||||
* @return 返回结果 [{
|
||||
label: '一级 1',
|
||||
children: [{
|
||||
label: '二级 1-1',
|
||||
children: [{
|
||||
label: '三级 1-1-1'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
*/
|
||||
convertToTreeData(data, pid) {
|
||||
const result = []
|
||||
const root = {
|
||||
label: data.length === 0 ? '无服务' : '服务列表',
|
||||
parentId: pid
|
||||
}
|
||||
const children = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const child = { parentId: 1, label: data[i] }
|
||||
children.push(child)
|
||||
}
|
||||
root.children = children
|
||||
result.push(root)
|
||||
return result
|
||||
},
|
||||
// table
|
||||
loadTable: function() {
|
||||
this.post('config.limit.list', this.searchFormData, function(resp) {
|
||||
this.pageInfo = resp.data
|
||||
})
|
||||
},
|
||||
loadRouteList: function(serviceId) {
|
||||
this.post('route.list/1.2', { serviceId: serviceId }, function(resp) {
|
||||
this.routeList = resp.data
|
||||
})
|
||||
},
|
||||
onAdd: function() {
|
||||
if (!this.serviceId) {
|
||||
this.tip('请选择服务', 'info')
|
||||
return
|
||||
}
|
||||
this.dlgTitle = '新增限流'
|
||||
this.limitDialogFormData.id = 0
|
||||
this.limitDialogVisible = true
|
||||
},
|
||||
onSearchTable: function() {
|
||||
this.searchFormData.pageIndex = 1
|
||||
this.loadTable()
|
||||
},
|
||||
onTableUpdate: function(row) {
|
||||
this.dlgTitle = '修改限流'
|
||||
this.limitDialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
Object.assign(this.limitDialogFormData, row)
|
||||
if (row.routeId) {
|
||||
this.limitDialogFormData.typeKey.push(1)
|
||||
}
|
||||
if (row.appKey) {
|
||||
this.limitDialogFormData.typeKey.push(2)
|
||||
}
|
||||
if (row.limitIp) {
|
||||
this.limitDialogFormData.typeKey.push(3)
|
||||
}
|
||||
})
|
||||
},
|
||||
onLimitDialogClose: function() {
|
||||
this.resetForm('limitDialogForm')
|
||||
this.limitDialogFormData.limitStatus = 0
|
||||
},
|
||||
infoRender: function(row) {
|
||||
if (row.limitType === 1) {
|
||||
const durationSeconds = row.durationSeconds
|
||||
return `每 ${durationSeconds} 秒可处理 ${row.execCountPerSecond} 个请求`
|
||||
} else if (row.limitType === 2) {
|
||||
return `令牌桶容量:${row.tokenBucketCount}`
|
||||
}
|
||||
},
|
||||
onLimitDialogSave: function() {
|
||||
this.$refs['limitDialogForm'].validate((valid) => {
|
||||
if (valid) {
|
||||
this.cleanCheckboxData()
|
||||
this.limitDialogFormData.serviceId = this.serviceId
|
||||
const uri = this.limitDialogFormData.id ? 'config.limit.update' : 'config.limit.add'
|
||||
this.post(uri, this.limitDialogFormData, function(resp) {
|
||||
this.limitDialogVisible = false
|
||||
this.loadTable()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
cleanCheckboxData: function() {
|
||||
// 如果没有勾选则清空
|
||||
if (!this.checkTypeKey(1)) {
|
||||
this.limitDialogFormData.routeId = ''
|
||||
}
|
||||
if (!this.checkTypeKey(2)) {
|
||||
this.limitDialogFormData.appKey = ''
|
||||
}
|
||||
if (!this.checkTypeKey(3)) {
|
||||
this.limitDialogFormData.limitIp = ''
|
||||
}
|
||||
},
|
||||
onLimitTypeTipClick: function() {
|
||||
const windowRemark = '窗口策略:每秒处理固定数量的请求,超出请求数量返回错误信息。'
|
||||
const tokenRemark = '令牌桶策略:每秒放置固定数量的令牌数,每个请求进来后先去拿令牌,拿到了令牌才能继续,拿不到则等候令牌重新生成了再拿。'
|
||||
const content = windowRemark + '<br>' + tokenRemark
|
||||
this.$alert(content, '限流策略', {
|
||||
dangerouslyUseHTMLString: true
|
||||
})
|
||||
},
|
||||
onSizeChange: function(size) {
|
||||
this.searchFormData.pageSize = size
|
||||
this.loadTable()
|
||||
},
|
||||
onPageIndexChange: function(pageIndex) {
|
||||
this.searchFormData.pageIndex = pageIndex
|
||||
this.loadTable()
|
||||
},
|
||||
checkTypeKey: function(val) {
|
||||
return this.limitDialogFormData.typeKey.find((value, index, arr) => {
|
||||
return value === val
|
||||
})
|
||||
},
|
||||
isWindowType: function() {
|
||||
return this.limitDialogFormData.limitType === 1
|
||||
},
|
||||
isTokenType: function() {
|
||||
return this.limitDialogFormData.limitType === 2
|
||||
},
|
||||
limitRender: function(row) {
|
||||
const html = []
|
||||
const val = []
|
||||
if (row.routeId) {
|
||||
val.push(row.routeId)
|
||||
html.push('路由ID')
|
||||
}
|
||||
if (row.appKey) {
|
||||
val.push(row.appKey)
|
||||
html.push('AppId')
|
||||
}
|
||||
if (row.limitIp) {
|
||||
val.push(row.limitIp)
|
||||
html.push('IP')
|
||||
}
|
||||
return val.join(' + ') + '<br>(' + html.join(' + ') + ')'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
230
sop-admin/sop-admin-frontend/src/views/service/log.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-plus" @click="onAddServer">添加监控服务器</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
style="width: 100%;margin-bottom: 20px;"
|
||||
border
|
||||
:default-expand-all="true"
|
||||
row-key="treeId"
|
||||
empty-text="请添加监控服务器"
|
||||
>
|
||||
<el-table-column
|
||||
prop="monitorName"
|
||||
label="网关实例"
|
||||
width="300"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.parentId === 0">{{ scope.row.monitorName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="serviceId"
|
||||
label="serviceId"
|
||||
width="200"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.parentId > 0">{{ scope.row.serviceId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="接口名 (版本号)"
|
||||
width="200"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.name + (scope.row.version ? ' (' + scope.row.version + ')' : '') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="count"
|
||||
label="出错次数"
|
||||
width="100"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="errorMsg"
|
||||
label="报错信息"
|
||||
width="300"
|
||||
>
|
||||
<template v-if="scope.row.parentId > 0" slot-scope="scope">
|
||||
<div style="display: inline-block;" v-html="showErrorMsg(scope.row)"></div> <el-button type="text" size="mini" @click="onShowErrorDetail(scope.row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="180"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="scope.row.parentId === 0 && scope.row.children" type="text" size="mini" @click="onClearLog(scope.row)">清空日志</el-button>
|
||||
<el-button v-if="scope.row.parentId === 0" type="text" size="mini" @click="onDelete(scope.row)">删除实例</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- dialog -->
|
||||
<el-dialog
|
||||
title="选择服务器实例"
|
||||
:visible.sync="logDialogInstanceVisible"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="logDialogForm"
|
||||
:model="logDialogFormData"
|
||||
:rules="rulesLog"
|
||||
label-width="150px"
|
||||
size="mini"
|
||||
>
|
||||
<el-form-item>
|
||||
<p style="color: #878787;">只能选择网关实例,其它实例不支持</p>
|
||||
</el-form-item>
|
||||
<el-form-item prop="instanceData" label="服务器实例">
|
||||
<el-select v-model="logDialogFormData.instanceData" value-key="id" style="width: 400px;">
|
||||
<el-option
|
||||
v-for="item in serviceData"
|
||||
:key="item.id"
|
||||
:label="item.serviceId + '(' + item.ipPort + ')'"
|
||||
:value="item"
|
||||
:disabled="isOptionDisabled(item)"
|
||||
>
|
||||
<span style="float: left">{{ item.serviceId }} <span v-if="isOptionDisabled(item)">(已添加)</span></span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.ipPort }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="logDialogInstanceVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onLogDialogSave">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
title="错误详情"
|
||||
:visible.sync="logDetailVisible"
|
||||
width="60%"
|
||||
>
|
||||
<div style="overflow-x: auto" v-html="errorMsgDetail"></div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="logDetailVisible = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
searchFormData: {},
|
||||
tableData: [],
|
||||
serviceData: [],
|
||||
// 已经添加的实例
|
||||
addedInstanceList: [],
|
||||
logDialogFormData: {
|
||||
instanceData: null
|
||||
},
|
||||
logDialogInstanceVisible: false,
|
||||
logDetailVisible: false,
|
||||
rulesLog: {
|
||||
instanceData: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
errorMsgDetail: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadServiceInstance()
|
||||
this.loadTable()
|
||||
},
|
||||
methods: {
|
||||
loadServiceInstance: function() {
|
||||
this.post('service.instance.list', {}, function(resp) {
|
||||
this.serviceData = resp.data.filter(el => {
|
||||
return el.instanceId && el.instanceId.length > 0
|
||||
})
|
||||
})
|
||||
this.post('monitor.instance.list', {}, function(resp) {
|
||||
this.addedInstanceList = resp.data
|
||||
})
|
||||
},
|
||||
loadTable: function() {
|
||||
this.post('monitor.log.list', {}, function(resp) {
|
||||
this.tableData = this.buildTreeData(resp.data)
|
||||
})
|
||||
},
|
||||
isOptionDisabled: function(item) {
|
||||
const ipPort = item.ipPort
|
||||
const index = this.addedInstanceList.findIndex((value, index, arr) => {
|
||||
return value === ipPort
|
||||
})
|
||||
return index > -1
|
||||
},
|
||||
buildTreeData: function(data) {
|
||||
data.forEach(ele => {
|
||||
const parentId = ele.parentId
|
||||
if (parentId === 0) {
|
||||
// 是根元素 ,不做任何操作,如果是正常的for-i循环,可以直接continue.
|
||||
} else {
|
||||
// 如果ele是子元素的话 ,把ele扔到他的父亲的child数组中.
|
||||
data.forEach(d => {
|
||||
if (d.treeId === parentId) {
|
||||
let childArray = d.children
|
||||
if (!childArray) {
|
||||
childArray = []
|
||||
}
|
||||
childArray.push(ele)
|
||||
d.children = childArray
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// 去除重复元素
|
||||
data = data.filter(ele => ele.parentId === 0)
|
||||
return data
|
||||
},
|
||||
showErrorMsg: function(row) {
|
||||
const msg = row.errorMsg.replace(/\<br\>/g, '')
|
||||
return msg.substring(0, 30) + '...'
|
||||
},
|
||||
onAddServer: function() {
|
||||
this.logDialogInstanceVisible = true
|
||||
},
|
||||
onDelete: function(row) {
|
||||
this.confirm('确定要删除实例【' + row.monitorName + '】吗?', function(done) {
|
||||
this.post('monitor.instance.del', { id: row.rawId }, function(resp) {
|
||||
done()
|
||||
this.tip('删除成功')
|
||||
this.loadTable()
|
||||
})
|
||||
})
|
||||
},
|
||||
onClearLog: function(row) {
|
||||
this.confirm('确定要清空日志吗?', function(done) {
|
||||
this.post('monitor.log.clear', { id: row.rawId }, function(resp) {
|
||||
done()
|
||||
this.tip('清空成功')
|
||||
this.loadTable()
|
||||
})
|
||||
})
|
||||
},
|
||||
onShowErrorDetail: function(row) {
|
||||
this.errorMsgDetail = row.errorMsg
|
||||
this.logDetailVisible = true
|
||||
},
|
||||
onLogDialogSave: function() {
|
||||
this.$refs['logDialogForm'].validate((valid) => {
|
||||
if (valid) {
|
||||
const instanceData = this.logDialogFormData.instanceData
|
||||
this.post('monitor.instance.add', instanceData, function(resp) {
|
||||
this.logDialogInstanceVisible = false
|
||||
this.loadTable()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
150
sop-admin/sop-admin-frontend/src/views/service/monitor.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
|
||||
<el-form-item label="接口名">
|
||||
<el-input v-model="searchFormData.routeId" :clearable="true" placeholder="输入接口名或版本号" style="width: 250px;" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="loadTable">搜索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-alert
|
||||
title="监控数据保存在网关服务器,重启网关数据会清空。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 10px"
|
||||
/>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
border
|
||||
:default-expand-all="false"
|
||||
row-key="id"
|
||||
height="500"
|
||||
empty-text="无数据"
|
||||
>
|
||||
<el-table-column
|
||||
fixed
|
||||
prop="instanceId"
|
||||
label="网关实例"
|
||||
width="200"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="!scope.row.children">{{ scope.row.instanceId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
fixed
|
||||
prop="name"
|
||||
label="接口名 (版本号)"
|
||||
width="280"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.name + (scope.row.version ? ' (' + scope.row.version + ')' : '') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="serviceId"
|
||||
label="serviceId"
|
||||
width="170"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="maxTime"
|
||||
label="最大耗时(ms)"
|
||||
width="125"
|
||||
>
|
||||
<template slot="header">
|
||||
最大耗时(ms)
|
||||
<el-tooltip effect="dark" content="耗时计算:签名验证成功后开始,微服务返回结果后结束" placement="top">
|
||||
<i class="el-icon-question" style="cursor: pointer"></i>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="minTime"
|
||||
label="最小耗时(ms)"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="avgTime"
|
||||
label="平均耗时(ms)"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="totalCount"
|
||||
label="总调用次数"
|
||||
width="100"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="successCount"
|
||||
label="成功次数"
|
||||
width="100"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="errorCount"
|
||||
label="失败次数"
|
||||
width="100"
|
||||
>
|
||||
<template slot="header">
|
||||
失败次数
|
||||
<el-tooltip effect="dark" content="只统计微服务返回的未知错误,JSR-303验证错误算作成功" placement="top-end">
|
||||
<i class="el-icon-question" style="cursor: pointer"></i>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<el-link
|
||||
v-if="scope.row.errorCount > 0"
|
||||
:underline="false"
|
||||
type="danger"
|
||||
style="text-decoration: underline;"
|
||||
@click="onShowErrorDetail(scope.row)"
|
||||
>
|
||||
{{ scope.row.errorCount }}
|
||||
</el-link>
|
||||
<span v-if="scope.row.errorCount === 0">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- dialog -->
|
||||
<el-dialog
|
||||
title="错误详情"
|
||||
:visible.sync="logDetailVisible"
|
||||
width="60%"
|
||||
>
|
||||
<div style="overflow-x: auto" v-html="errorMsgDetail"></div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="logDetailVisible = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
searchFormData: {
|
||||
routeId: ''
|
||||
},
|
||||
tableData: [],
|
||||
logDetailVisible: false,
|
||||
errorMsgDetail: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTable()
|
||||
},
|
||||
methods: {
|
||||
loadTable: function() {
|
||||
this.post('monitor.data.list', this.searchFormData, function(resp) {
|
||||
const data = resp.data
|
||||
this.tableData = data.monitorInfoData
|
||||
})
|
||||
},
|
||||
onShowErrorDetail: function(row) {
|
||||
const errorMsgList = row.errorMsgList
|
||||
this.errorMsgDetail = errorMsgList.length > 0 ? errorMsgList.join('<br>') : '无内容'
|
||||
this.logDetailVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
283
sop-admin/sop-admin-frontend/src/views/service/monitorNew.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini" @submit.native.prevent>
|
||||
<el-form-item label="接口名">
|
||||
<el-input v-model="searchFormData.routeId" :clearable="true" style="width: 250px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="serviceId">
|
||||
<el-input v-model="searchFormData.serviceId" :clearable="true" style="width: 250px;" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" native-type="submit" @click="loadTable">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
:data="pageInfo.list"
|
||||
row-key="id"
|
||||
lazy
|
||||
empty-text="无数据"
|
||||
:load="loadInstanceMonitorInfo"
|
||||
>
|
||||
<el-table-column
|
||||
fixed
|
||||
prop="instanceId"
|
||||
label="网关实例"
|
||||
width="200"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="!scope.row.children">{{ scope.row.instanceId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
fixed
|
||||
prop="name"
|
||||
label="接口名 (版本号)"
|
||||
width="200"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.name + (scope.row.version ? ' (' + scope.row.version + ')' : '') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="serviceId"
|
||||
label="serviceId"
|
||||
width="150"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="maxTime"
|
||||
label="最大耗时(ms)"
|
||||
>
|
||||
<template slot="header">
|
||||
最大耗时(ms)
|
||||
<el-tooltip content="耗时计算:签名验证成功后开始,应用返回结果后结束" placement="top">
|
||||
<i class="el-icon-question" style="cursor: pointer"></i>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="minTime"
|
||||
label="最小耗时(ms)"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="avgTime"
|
||||
label="平均耗时(ms)"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.avgTime.toFixed(1) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="totalRequestCount"
|
||||
label="总调用次数"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="successCount"
|
||||
label="成功次数"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="errorCount"
|
||||
label="失败次数"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="unsolvedErrorCount"
|
||||
label="未解决错误"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-link
|
||||
v-if="scope.row.unsolvedErrorCount > 0"
|
||||
:underline="false"
|
||||
type="danger"
|
||||
style="text-decoration: underline;"
|
||||
@click="onShowErrorDetail(scope.row)"
|
||||
>
|
||||
{{ scope.row.unsolvedErrorCount }}
|
||||
</el-link>
|
||||
<span v-if="scope.row.unsolvedErrorCount === 0">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
style="margin-top: 5px"
|
||||
:current-page="searchFormData.pageIndex"
|
||||
:page-size="searchFormData.pageSize"
|
||||
:page-sizes="[5, 10, 20, 40]"
|
||||
:total="pageInfo.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onPageIndexChange"
|
||||
/>
|
||||
<!-- dialog -->
|
||||
<el-dialog
|
||||
:title="errorMsgData.title"
|
||||
:visible.sync="logDetailVisible"
|
||||
:close-on-click-modal="false"
|
||||
width="70%"
|
||||
@close="onCloseErrorDlg"
|
||||
>
|
||||
<el-alert
|
||||
title="修复错误后请标记解决"
|
||||
:closable="false"
|
||||
class="el-alert-tip"
|
||||
/>
|
||||
<el-table
|
||||
:data="errorMsgData.pageInfo.rows"
|
||||
empty-text="无错误日志"
|
||||
>
|
||||
<el-table-column
|
||||
type="expand"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<el-input v-model="props.row.errorMsg" type="textarea" :rows="8" readonly />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="errorMsg"
|
||||
label="错误内容"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span v-if="props.row.errorMsg.length > 50">{{ props.row.errorMsg.substring(0, 50) }}...</span>
|
||||
<span v-else>{{ props.row.errorMsg }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="instanceId"
|
||||
label="实例ID"
|
||||
width="150px"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="count"
|
||||
label="报错次数"
|
||||
width="80px"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="gmtModified"
|
||||
label="报错时间"
|
||||
width="160px"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="120"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-link type="primary" @click="onSolve(scope.row)">标记解决</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
style="margin-top: 5px"
|
||||
:current-page="errorMsgFormData.pageIndex"
|
||||
:page-size="errorMsgFormData.pageSize"
|
||||
:page-sizes="[5, 10, 20, 40]"
|
||||
:total="errorMsgData.pageInfo.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="onErrorSizeChange"
|
||||
@current-change="onErrorPageIndexChange"
|
||||
/>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="logDetailVisible = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
searchFormData: {
|
||||
routeId: '',
|
||||
serviceId: '',
|
||||
pageIndex: 1,
|
||||
pageSize: 20
|
||||
},
|
||||
pageInfo: {
|
||||
list: [],
|
||||
total: 0
|
||||
},
|
||||
logDetailVisible: false,
|
||||
errorMsgFormData: {
|
||||
routeId: '',
|
||||
instanceId: '',
|
||||
pageIndex: 1,
|
||||
pageSize: 5
|
||||
},
|
||||
errorMsgData: {
|
||||
title: '',
|
||||
name: '',
|
||||
version: '',
|
||||
pageInfo: {
|
||||
rows: [],
|
||||
total: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTable()
|
||||
},
|
||||
methods: {
|
||||
loadTable: function() {
|
||||
this.post('monitornew.data.page', this.searchFormData, function(resp) {
|
||||
this.pageInfo = resp.data
|
||||
})
|
||||
},
|
||||
loadErrorData: function() {
|
||||
this.post('monitornew.error.page', this.errorMsgFormData, function(resp) {
|
||||
this.errorMsgData.pageInfo = resp.data
|
||||
this.logDetailVisible = true
|
||||
})
|
||||
},
|
||||
loadInstanceMonitorInfo(row, treeNode, resolve) {
|
||||
this.post('monitornew.routeid.data.get', { routeId: row.routeId }, resp => {
|
||||
const children = resp.data
|
||||
row.children = children
|
||||
resolve(children)
|
||||
})
|
||||
},
|
||||
onShowErrorDetail: function(row) {
|
||||
this.errorMsgData.title = `错误日志 ${row.name}(${row.version})`
|
||||
this.errorMsgData.name = row.name
|
||||
this.errorMsgData.version = row.version
|
||||
this.errorMsgFormData.routeId = row.routeId
|
||||
this.errorMsgFormData.instanceId = row.instanceId
|
||||
this.loadErrorData()
|
||||
},
|
||||
onSolve: function(row) {
|
||||
this.confirm('确认标记为已解决吗?', function(done) {
|
||||
this.post('monitornew.error.solve', { routeId: row.routeId, errorId: row.errorId }, function(resp) {
|
||||
done()
|
||||
this.errorMsgFormData.pageIndex = 1
|
||||
this.loadErrorData()
|
||||
})
|
||||
})
|
||||
},
|
||||
onCloseErrorDlg: function() {
|
||||
this.loadTable()
|
||||
},
|
||||
onErrorSizeChange: function(size) {
|
||||
this.errorMsgFormData.pageSize = size
|
||||
this.loadErrorData()
|
||||
},
|
||||
onErrorPageIndexChange: function(pageIndex) {
|
||||
this.errorMsgFormData.pageIndex = pageIndex
|
||||
this.loadErrorData()
|
||||
},
|
||||
onSizeChange: function(size) {
|
||||
this.searchFormData.pageSize = size
|
||||
this.loadTable()
|
||||
},
|
||||
onPageIndexChange: function(pageIndex) {
|
||||
this.searchFormData.pageIndex = pageIndex
|
||||
this.loadTable()
|
||||
},
|
||||
onAdd: function() {
|
||||
this.dialogTitle = '新增IP'
|
||||
this.dialogVisible = true
|
||||
this.dialogFormData.id = 0
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
557
sop-admin/sop-admin-frontend/src/views/service/route.vue
Normal file
@@ -0,0 +1,557 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div v-if="tabsData.length === 0">
|
||||
无服务
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tabs v-model="tabsActive" type="card" @tab-click="selectTab">
|
||||
<el-tab-pane v-for="tabName in tabsData" :key="tabName" :label="tabName" :name="tabName" />
|
||||
</el-tabs>
|
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini" @submit.native.prevent>
|
||||
<el-form-item label="路由名称">
|
||||
<el-input v-model="searchFormData.id" :clearable="true" placeholder="输入接口名或版本号" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="searchFormData.permission">授权接口</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="searchFormData.needToken">需要token</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" native-type="submit" @click="onSearchTable">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button
|
||||
v-show="isCustomService"
|
||||
type="primary"
|
||||
size="mini"
|
||||
icon="el-icon-plus"
|
||||
@click.stop="addRoute"
|
||||
>
|
||||
新建路由
|
||||
</el-button>
|
||||
<el-table
|
||||
:data="pageInfo.rows"
|
||||
border
|
||||
highlight-current-row
|
||||
style="margin-top: 10px;"
|
||||
>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="接口名 (版本号)"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
{{ getNameVersion(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="roles"
|
||||
label="访问权限"
|
||||
width="150"
|
||||
:show-overflow-tooltip="true"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="!scope.row.permission">
|
||||
(公开)
|
||||
</span>
|
||||
<span v-else class="roles-content" @click="onTableAuth(scope.row)" v-html="roleRender(scope.row)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="ignoreValidate"
|
||||
label="签名校验"
|
||||
width="120"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.ignoreValidate === 0">校验</span>
|
||||
<span v-if="scope.row.ignoreValidate === 1" style="color:#E6A23C">不校验</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="mergeResult"
|
||||
label="统一格式输出"
|
||||
width="120"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.mergeResult === 1">是</span>
|
||||
<span v-if="scope.row.mergeResult === 0" style="color:#E6A23C">否</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="needToken"
|
||||
label="需要token"
|
||||
width="120"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.needToken === 1" style="font-weight: bold;color: #303133;">是</span>
|
||||
<span v-if="scope.row.needToken === 0">否</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="状态"
|
||||
width="80"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.status"
|
||||
active-color="#13ce66"
|
||||
inactive-color="#ff4949"
|
||||
:active-value="1"
|
||||
:inactive-value="2"
|
||||
@change="onChangeStatus(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
style="margin-top: 5px"
|
||||
:current-page="searchFormData.pageIndex"
|
||||
:page-size="searchFormData.pageSize"
|
||||
:page-sizes="[10, 20, 40]"
|
||||
:total="pageInfo.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onPageIndexChange"
|
||||
/>
|
||||
</div>
|
||||
<!-- route dialog -->
|
||||
<el-dialog
|
||||
:title="routeDialogTitle"
|
||||
:visible.sync="routeDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
@close="onCloseRouteDialog"
|
||||
>
|
||||
<el-form
|
||||
ref="routeDialogFormRef"
|
||||
:model="routeDialogFormData"
|
||||
:rules="routeDialogFormRules"
|
||||
label-width="180px"
|
||||
size="mini"
|
||||
>
|
||||
<el-input v-show="false" v-model="routeDialogFormData.id" />
|
||||
<el-form-item label="接口名 (版本号)">
|
||||
{{ routeDialogFormData.name + (routeDialogFormData.version ? ' (' + routeDialogFormData.version + ')' : '') }}
|
||||
</el-form-item>
|
||||
<el-form-item label="签名校验">
|
||||
{{ routeDialogFormData.ignoreValidate ? '不校验' : '校验' }}
|
||||
</el-form-item>
|
||||
<el-form-item label="统一格式输出">
|
||||
{{ routeDialogFormData.mergeResult === 1 ? '是' : '否' }}
|
||||
</el-form-item>
|
||||
<el-form-item label="需要token">
|
||||
{{ routeDialogFormData.needToken === 1 ? '是' : '否' }}
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="routeDialogFormData.status">
|
||||
<el-radio :label="1" name="status">启用</el-radio>
|
||||
<el-radio :label="2" name="status" style="color:#F56C6C">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="routeDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onRouteDialogSave">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- auth dialog -->
|
||||
<el-dialog
|
||||
title="路由授权"
|
||||
:visible.sync="authDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
:model="authDialogFormData"
|
||||
label-width="120px"
|
||||
size="mini"
|
||||
>
|
||||
<el-form-item label="路由ID">
|
||||
<span>{{ authDialogFormData.routeId }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-checkbox-group v-model="authDialogFormData.roleCode">
|
||||
<el-checkbox v-for="item in roles" :key="item.roleCode" :label="item.roleCode">{{ item.description }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="authDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onAuthDialogSave">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!--添加服务-->
|
||||
<el-dialog
|
||||
title="添加服务"
|
||||
:visible.sync="addServiceDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
@close="closeAddServiceDlg"
|
||||
>
|
||||
<el-form
|
||||
ref="addServiceForm"
|
||||
:model="addServiceForm"
|
||||
:rules="addServiceFormRules"
|
||||
label-width="200px"
|
||||
>
|
||||
<el-form-item label="服务名(serviceId)" prop="serviceId">
|
||||
<el-input v-model="addServiceForm.serviceId" placeholder="服务名,如:order-service" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="addServiceDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onAddService">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.custom-tree-node {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.el-input.is-disabled .el-input__inner {color: #909399;}
|
||||
.el-radio__input.is-disabled+span.el-radio__label {color: #909399;}
|
||||
.roles-content { cursor: pointer;color: #20a0ff }
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tabsData: [],
|
||||
tabsActive: '',
|
||||
serviceTextLimitSize: 20,
|
||||
filterText: '',
|
||||
treeData: [],
|
||||
tableData: [],
|
||||
serviceId: '',
|
||||
isCustomService: false,
|
||||
searchFormData: {
|
||||
id: '',
|
||||
serviceId: '',
|
||||
permission: 0,
|
||||
needToken: 0,
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
pageInfo: {
|
||||
rows: [],
|
||||
total: 0
|
||||
},
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'label'
|
||||
},
|
||||
routeDialogTitle: '修改路由',
|
||||
// dialog
|
||||
routeDialogFormData: {
|
||||
id: '',
|
||||
name: '',
|
||||
version: '1.0',
|
||||
uri: '',
|
||||
path: '',
|
||||
status: 1,
|
||||
mergeResult: 1,
|
||||
ignoreValidate: 0
|
||||
},
|
||||
routeDialogFormRules: {
|
||||
name: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
version: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
uri: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
path: [
|
||||
{ min: 0, max: 100, message: '长度不能超过 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
routeDialogVisible: false,
|
||||
roles: [],
|
||||
authDialogFormData: {
|
||||
routeId: '',
|
||||
roleCode: []
|
||||
},
|
||||
authDialogVisible: false,
|
||||
// addService
|
||||
addServiceDialogVisible: false,
|
||||
addServiceForm: {
|
||||
serviceId: ''
|
||||
},
|
||||
addServiceFormRules: {
|
||||
serviceId: [
|
||||
{ required: true, message: '请输入服务名称', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filterText(val) {
|
||||
this.$refs.serviceTree.filter(val)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTabs()
|
||||
this.loadRouteRole()
|
||||
},
|
||||
methods: {
|
||||
loadTabs() {
|
||||
this.post('registry.service.list', {}, function(resp) {
|
||||
this.tabsData = resp.data
|
||||
this.$nextTick(() => {
|
||||
if (this.tabsData.length > 0) {
|
||||
this.tabsActive = this.tabsData[0]
|
||||
this.loadRouteData()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
// 加载树
|
||||
loadTree: function() {
|
||||
this.post('registry.service.list', {}, function(resp) {
|
||||
const respData = resp.data
|
||||
this.treeData = this.convertToTreeData(respData, 0)
|
||||
this.$nextTick(() => {
|
||||
// 高亮已选中的
|
||||
if (this.serviceId) {
|
||||
this.$refs.serviceTree.setCurrentKey(this.serviceId)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
// 树搜索
|
||||
filterNode(value, data) {
|
||||
if (!value) return true
|
||||
return data.label.indexOf(value) !== -1
|
||||
},
|
||||
// 树点击事件
|
||||
onNodeClick(data, node, tree) {
|
||||
if (data.parentId) {
|
||||
this.serviceId = data.label
|
||||
this.searchFormData.serviceId = this.serviceId
|
||||
this.isCustomService = Boolean(data.custom)
|
||||
this.loadTable()
|
||||
}
|
||||
},
|
||||
selectTab() {
|
||||
this.loadRouteData()
|
||||
},
|
||||
loadRouteData() {
|
||||
this.serviceId = this.tabsActive
|
||||
this.searchFormData.serviceId = this.serviceId
|
||||
this.loadTable()
|
||||
},
|
||||
/**
|
||||
* 数组转成树状结构
|
||||
* @param data 数据结构 [{
|
||||
"_parentId": 14,
|
||||
"gmtCreate": "2019-01-15 09:44:38",
|
||||
"gmtUpdate": "2019-01-15 09:44:38",
|
||||
"id": 15,
|
||||
"isShow": 1,
|
||||
"name": "用户注册",
|
||||
"orderIndex": 10000,
|
||||
"parentId": 14
|
||||
},...]
|
||||
* @param pid 初始父节点id,一般是0
|
||||
* @return 返回结果 [{
|
||||
label: '一级 1',
|
||||
children: [{
|
||||
label: '二级 1-1',
|
||||
children: [{
|
||||
label: '三级 1-1-1'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
*/
|
||||
convertToTreeData(data, pid) {
|
||||
const result = []
|
||||
const root = {
|
||||
label: data.length === 0 ? '无服务' : '服务列表',
|
||||
parentId: pid
|
||||
}
|
||||
const children = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const child = { parentId: 1, label: data[i] }
|
||||
children.push(child)
|
||||
}
|
||||
root.children = children
|
||||
result.push(root)
|
||||
return result
|
||||
},
|
||||
getNameVersion(row) {
|
||||
return row.name + (row.version ? ' (' + row.version + ')' : '')
|
||||
},
|
||||
// table
|
||||
loadTable: function(param) {
|
||||
if (!this.searchFormData.serviceId) {
|
||||
this.tip('请选择一个服务', 'error')
|
||||
return
|
||||
}
|
||||
const postData = param || this.searchFormData
|
||||
this.post('route.page', postData, function(resp) {
|
||||
this.pageInfo = resp.data
|
||||
})
|
||||
},
|
||||
onSearchTable: function() {
|
||||
this.searchFormData.pageIndex = 1
|
||||
this.loadTable()
|
||||
},
|
||||
onTableUpdate: function(row) {
|
||||
this.routeDialogTitle = '修改路由'
|
||||
this.routeDialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
Object.assign(this.routeDialogFormData, row)
|
||||
})
|
||||
},
|
||||
onTableAuth: function(row) {
|
||||
this.authDialogFormData.routeId = row.id
|
||||
const searchData = { id: row.id, serviceId: this.serviceId }
|
||||
this.post('route.role.get', searchData, function(resp) {
|
||||
const roleList = resp.data
|
||||
const roleCodes = []
|
||||
for (let i = 0; i < roleList.length; i++) {
|
||||
roleCodes.push(roleList[i].roleCode)
|
||||
}
|
||||
this.authDialogFormData.roleCode = roleCodes
|
||||
this.authDialogVisible = true
|
||||
})
|
||||
},
|
||||
onTableDel: function(row) {
|
||||
this.confirm(`确认要删除路由【${row.id}】吗?`, function(done) {
|
||||
const data = {
|
||||
serviceId: this.serviceId,
|
||||
id: row.id
|
||||
}
|
||||
this.post('route.del', data, function() {
|
||||
done()
|
||||
this.tip('删除成功')
|
||||
this.loadTable()
|
||||
})
|
||||
})
|
||||
},
|
||||
// element-ui switch开关 点击按钮后,弹窗确认后再改变开关状态
|
||||
// https://blog.csdn.net/Gomeer/article/details/103697593
|
||||
onChangeStatus: function(row) {
|
||||
const newStatus = row.status
|
||||
const oldStatus = newStatus === 1 ? 2 : 1
|
||||
// 先将状态改成原来的值
|
||||
row.status = oldStatus
|
||||
const nameVersion = this.getNameVersion(row)
|
||||
const msg = oldStatus === 1 ? `确认要禁用 ${nameVersion} 吗?` : `确认要启用 ${nameVersion} 吗?`
|
||||
this.confirm(msg, function(done) {
|
||||
const data = {
|
||||
id: row.id,
|
||||
status: newStatus
|
||||
}
|
||||
// 'route.role.update', this.authDialogFormData
|
||||
this.post('route.status.update', data, function() {
|
||||
done()
|
||||
row.status = newStatus
|
||||
})
|
||||
}, (done) => {
|
||||
row.status = oldStatus
|
||||
done()
|
||||
})
|
||||
},
|
||||
onCloseRouteDialog: function() {
|
||||
this.resetForm('routeDialogFormRef')
|
||||
},
|
||||
routePropDisabled: function() {
|
||||
if (!this.routeDialogFormData.id) {
|
||||
return false
|
||||
}
|
||||
return !this.isCustomService
|
||||
},
|
||||
loadRouteRole: function() {
|
||||
if (this.roles.length === 0) {
|
||||
this.post('role.listall', {}, function(resp) {
|
||||
this.roles = resp.data
|
||||
})
|
||||
}
|
||||
},
|
||||
addRoute: function() {
|
||||
this.routeDialogTitle = '新建路由'
|
||||
this.routeDialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
Object.assign(this.routeDialogFormData, {
|
||||
id: ''
|
||||
})
|
||||
})
|
||||
},
|
||||
roleRender: function(row) {
|
||||
const html = []
|
||||
const roles = row.roles
|
||||
for (let i = 0; i < roles.length; i++) {
|
||||
html.push(roles[i].description)
|
||||
}
|
||||
return html.length > 0 ? html.join(', ') : '点击授权'
|
||||
},
|
||||
onRouteDialogSave: function() {
|
||||
this.$refs.routeDialogFormRef.validate((valid) => {
|
||||
if (valid) {
|
||||
const uri = this.routeDialogFormData.id ? 'route.status.update' : 'route.add'
|
||||
this.routeDialogFormData.serviceId = this.serviceId
|
||||
this.post(uri, this.routeDialogFormData, function() {
|
||||
this.routeDialogVisible = false
|
||||
this.loadTable()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onAuthDialogSave: function() {
|
||||
this.post('route.role.update', this.authDialogFormData, function() {
|
||||
this.authDialogVisible = false
|
||||
this.loadTable()
|
||||
})
|
||||
},
|
||||
addService: function() {
|
||||
this.addServiceDialogVisible = true
|
||||
},
|
||||
closeAddServiceDlg: function() {
|
||||
this.$refs.addServiceForm.resetFields()
|
||||
},
|
||||
onAddService: function() {
|
||||
this.$refs.addServiceForm.validate((valid) => {
|
||||
if (valid) {
|
||||
this.post('service.custom.add', this.addServiceForm, function(resp) {
|
||||
this.addServiceDialogVisible = false
|
||||
this.tip('添加成功')
|
||||
this.loadTree()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onDelService: function(data) {
|
||||
const serviceId = data.serviceId
|
||||
this.confirm('确认要删除服务' + serviceId + '吗,【对应的路由配置会一起删除】', function(done) {
|
||||
const postData = {
|
||||
serviceId: serviceId
|
||||
}
|
||||
this.post('service.custom.del', postData, function() {
|
||||
done()
|
||||
this.tip('删除成功')
|
||||
this.loadTree()
|
||||
})
|
||||
})
|
||||
},
|
||||
onSizeChange: function(size) {
|
||||
this.searchFormData.pageSize = size
|
||||
this.loadTable()
|
||||
},
|
||||
onPageIndexChange: function(pageIndex) {
|
||||
this.searchFormData.pageIndex = pageIndex
|
||||
this.loadTable()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
179
sop-admin/sop-admin-frontend/src/views/service/sdk.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form size="mini">
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-upload" @click="onAddSdk">发布SDK</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
:data="list"
|
||||
border
|
||||
>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="SDK"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="version"
|
||||
label="版本"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="content"
|
||||
label="下载地址"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-link type="primary" :href="scope.row.content" target="_blank">{{ scope.row.content }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
align="center"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="onSdkUpdate(scope.row)">编辑</el-button>
|
||||
<el-button type="text" size="mini" @click="onSdkDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--dialog-->
|
||||
<el-dialog
|
||||
:title="sdkDlgTitle"
|
||||
:visible.sync="sdkDlgAddShow"
|
||||
:close-on-click-modal="false"
|
||||
@close="resetForm('sdkAddForm')"
|
||||
>
|
||||
<el-form
|
||||
ref="sdkAddForm"
|
||||
:model="sdkFormAddData"
|
||||
:rules="sdkFormRule"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item prop="name" label="选择语言">
|
||||
<el-select
|
||||
v-model="sdkFormAddData.name"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in sdkConfigs"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="version" label="版本">
|
||||
<el-input v-model="sdkFormAddData.version" maxlength="30" show-word-limit placeholder="如:1.0" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="content" label="下载地址">
|
||||
<el-input v-model="sdkFormAddData.content" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item prop="extContent" label="调用示例">
|
||||
<el-input v-model="sdkFormAddData.extContent" type="textarea" :rows="12" placeholder="填写SDK调用示例代码,支持markdown语法" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="sdkDlgAddShow = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onSubmitForm">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const appFormDataInit = function() {
|
||||
return {
|
||||
id: 0,
|
||||
name: '',
|
||||
version: '',
|
||||
content: '',
|
||||
extContent: ''
|
||||
}
|
||||
}
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
searchFormData: {},
|
||||
sdkDownloadConfig: [],
|
||||
sdkConfigs: [],
|
||||
sdkDlgTitle: '',
|
||||
sdkDlgAddShow: false,
|
||||
sdkFormUpdateData: appFormDataInit(),
|
||||
sdkFormAddData: appFormDataInit(),
|
||||
sdkFormLoading: false,
|
||||
sdkFormRule: {
|
||||
name: [
|
||||
{ required: true, message: '请选择语言', trigger: 'blur' }
|
||||
],
|
||||
version: [
|
||||
{ required: true, message: '请填版本', trigger: 'blur' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: '请填写URL', trigger: 'blur' }
|
||||
],
|
||||
extContent: [
|
||||
{ required: true, message: '请填写调用示例', trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
downloadUrl: '',
|
||||
list: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadLangSelector()
|
||||
this.loadTable()
|
||||
},
|
||||
methods: {
|
||||
loadLangSelector: function() {
|
||||
this.getFile(`static/sdkConfig.json?q=${new Date().getTime()}`, (content) => {
|
||||
this.sdkConfigs = content.langList
|
||||
})
|
||||
},
|
||||
loadTable: function() {
|
||||
this.post('isp.sdk.list', this.searchFormData, resp => {
|
||||
this.list = resp.data
|
||||
})
|
||||
},
|
||||
onSizeChange: function(size) {
|
||||
this.searchFormData.pageSize = size
|
||||
this.loadTable()
|
||||
},
|
||||
onPageIndexChange: function(pageIndex) {
|
||||
this.searchFormData.pageIndex = pageIndex
|
||||
this.loadTable()
|
||||
},
|
||||
onAddSdk: function() {
|
||||
this.sdkDlgTitle = '添加SDK'
|
||||
this.sdkFormAddData = appFormDataInit()
|
||||
this.sdkDlgAddShow = true
|
||||
},
|
||||
onSdkUpdate: function(row) {
|
||||
this.sdkDlgTitle = '修改SDK'
|
||||
this.sdkFormAddData = appFormDataInit()
|
||||
Object.assign(this.sdkFormAddData, row)
|
||||
this.sdkDlgAddShow = true
|
||||
},
|
||||
onSdkDelete: function(row) {
|
||||
this.confirm(`确认要删除【${row.name}】吗?`, (done) => {
|
||||
this.post('isp.sdk.delete', { id: row.id }, resp => {
|
||||
done()
|
||||
this.tip('删除成功')
|
||||
this.loadTable()
|
||||
})
|
||||
})
|
||||
},
|
||||
onSubmitForm: function() {
|
||||
this.$refs.sdkAddForm.validate((valid) => {
|
||||
if (valid) {
|
||||
const uri = this.sdkFormAddData.id ? 'isp.sdk.update' : 'isp.sdk.add'
|
||||
this.post(uri, this.sdkFormAddData, function() {
|
||||
this.sdkDlgAddShow = false
|
||||
this.loadTable()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
429
sop-admin/sop-admin-frontend/src/views/service/serviceList.vue
Normal file
@@ -0,0 +1,429 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
|
||||
<el-form-item label="serviceId">
|
||||
<el-input v-model="searchFormData.serviceId" :clearable="true" placeholder="serviceId" style="width: 250px;" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="onSearchTable">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
style="width: 100%;margin-bottom: 20px;"
|
||||
border
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column
|
||||
prop="serviceId"
|
||||
label="服务名称"
|
||||
width="200"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-html="renderServiceName(scope.row)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="ipPort"
|
||||
label="IP端口"
|
||||
width="250"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="metadata"
|
||||
label="当前环境"
|
||||
width="100"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.status === 'UP'">
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.metadata.env === 'pre'" type="warning">预发布</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.metadata.env === 'gray'" type="info">灰度</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && !scope.row.metadata.env" type="success">线上</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="metadata"
|
||||
label="metadata"
|
||||
width="250"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.parentId > 0">{{ JSON.stringify(scope.row.metadata) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="服务状态"
|
||||
width="100"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.status === 'UP'" type="success">正常</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.status === 'STARTING'" type="info">正在启动</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && scope.row.status === 'UNKNOWN'">未知</el-tag>
|
||||
<el-tag v-if="scope.row.parentId > 0 && (scope.row.status === 'OUT_OF_SERVICE' || scope.row.status === 'DOWN')" type="danger">已禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="250"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<div v-if="blackList.indexOf(scope.row.serviceId.toLowerCase()) < 0">
|
||||
<div v-if="scope.row.parentId === 0">
|
||||
<el-button type="text" size="mini" @click="onGrayConfigUpdate(scope.row)">设置灰度参数</el-button>
|
||||
</div>
|
||||
<div v-if="scope.row.parentId > 0">
|
||||
<span v-if="scope.row.status === 'UP'">
|
||||
<el-button v-if="scope.row.metadata.env === 'pre'" type="text" size="mini" @click="onEnvPreClose(scope.row)">结束预发布</el-button>
|
||||
<el-button v-if="scope.row.metadata.env === 'gray'" type="text" size="mini" @click="onEnvGrayClose(scope.row)">结束灰度</el-button>
|
||||
<el-button v-if="!scope.row.metadata.env" type="text" size="mini" @click="onEnvPreOpen(scope.row)">开启预发布</el-button>
|
||||
<el-button v-if="!scope.row.metadata.env" type="text" size="mini" @click="onEnvGrayOpen(scope.row)">开启灰度</el-button>
|
||||
</span>
|
||||
<span style="margin-left: 10px;">
|
||||
<el-button v-if="scope.row.status === 'UP'" type="text" size="mini" @click="onDisable(scope.row)">禁用</el-button>
|
||||
<el-button v-if="scope.row.status === 'OUT_OF_SERVICE'" type="text" size="mini" @click="onEnable(scope.row)">启用</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- dialog -->
|
||||
<el-dialog
|
||||
title="灰度设置"
|
||||
:visible.sync="grayDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
@close="resetForm('grayForm')"
|
||||
>
|
||||
<el-form
|
||||
ref="grayForm"
|
||||
:model="grayForm"
|
||||
:rules="grayFormRules"
|
||||
size="mini"
|
||||
>
|
||||
<el-form-item label="serviceId">
|
||||
{{ grayForm.serviceId }}
|
||||
</el-form-item>
|
||||
<el-tabs v-model="tabsActiveName" type="card">
|
||||
<el-tab-pane label="灰度用户" name="first">
|
||||
<el-alert
|
||||
title="可以是AppId或IP地址,多个用英文逗号隔开"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
/>
|
||||
<el-form-item prop="userKeyContent">
|
||||
<el-input
|
||||
v-model="grayForm.userKeyContent"
|
||||
placeholder="可以是AppId或IP地址,多个用英文逗号隔开"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="接口配置" name="second">
|
||||
<el-alert
|
||||
title="灰度接口:接口名相同,版本号不同"
|
||||
type="info"
|
||||
:closable="false"
|
||||
/>
|
||||
<el-form-item>
|
||||
<el-button type="text" @click="addNameVersion">新增灰度接口</el-button>
|
||||
</el-form-item>
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<tr
|
||||
v-for="(grayRouteConfig, index) in grayForm.grayRouteConfigList"
|
||||
:key="grayRouteConfig.key"
|
||||
>
|
||||
<td>
|
||||
<el-form-item
|
||||
:key="grayRouteConfig.key"
|
||||
:prop="'grayRouteConfigList.' + index + '.oldRouteId'"
|
||||
:rules="{required: true, message: '不能为空', trigger: ['blur', 'change']}"
|
||||
>
|
||||
老接口:
|
||||
<el-select
|
||||
v-model="grayRouteConfig.oldRouteId"
|
||||
filterable
|
||||
style="margin-right: 10px;width: 250px"
|
||||
@change="onChangeOldRoute(grayRouteConfig)"
|
||||
>
|
||||
<el-option
|
||||
v-for="route in routeList"
|
||||
:key="route.id"
|
||||
:label="route.name + '(' + route.version + ')'"
|
||||
:value="route.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</td>
|
||||
<td>
|
||||
<el-form-item
|
||||
:key="grayRouteConfig.key + 1"
|
||||
:prop="'grayRouteConfigList.' + index + '.newVersion'"
|
||||
:rules="{required: true, message: '不能为空', trigger: ['blur', 'change']}"
|
||||
>
|
||||
灰度接口:
|
||||
<el-select
|
||||
v-model="grayRouteConfig.newVersion"
|
||||
filterable
|
||||
no-data-text="无数据"
|
||||
style="width: 250px"
|
||||
>
|
||||
<el-option
|
||||
v-for="routeNew in getGraySelectData(grayRouteConfig.oldRouteId)"
|
||||
:key="routeNew.id"
|
||||
:label="routeNew.name + '(' + routeNew.version + ')'"
|
||||
:value="routeNew.version"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</td>
|
||||
<td style="vertical-align: baseline;">
|
||||
<el-button v-show="grayForm.grayRouteConfigList.length > 1" type="text" @click.prevent="removeNameVersion(grayRouteConfig)">删除</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="grayDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="onGrayConfigSave">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
const regex = /^\S+(,\S+)*$/
|
||||
const userKeyContentValidator = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('不能为空'))
|
||||
} else {
|
||||
if (!regex.test(value)) {
|
||||
callback(new Error('格式不正确'))
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
searchFormData: {
|
||||
serviceId: ''
|
||||
},
|
||||
blackList: ['sop-gateway', 'sop-admin'],
|
||||
grayDialogVisible: false,
|
||||
grayForm: {
|
||||
serviceId: '',
|
||||
userKeyContent: '',
|
||||
onlyUpdateGrayUserkey: false,
|
||||
grayRouteConfigList: []
|
||||
},
|
||||
tabsActiveName: 'first',
|
||||
routeList: [],
|
||||
selectNameVersion: [],
|
||||
grayFormRules: {
|
||||
userKeyContent: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||
{ validator: userKeyContentValidator, trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
tableData: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadTable()
|
||||
},
|
||||
methods: {
|
||||
loadTable: function() {
|
||||
this.post('service.instance.list', this.searchFormData, function(resp) {
|
||||
this.tableData = this.buildTreeData(resp.data)
|
||||
})
|
||||
},
|
||||
loadRouteList: function(serviceId) {
|
||||
this.post('route.list/1.2', { serviceId: serviceId.toLowerCase() }, function(resp) {
|
||||
this.routeList = resp.data
|
||||
})
|
||||
},
|
||||
getGraySelectData: function(oldRouteId) {
|
||||
return this.routeList.filter(routeNew => {
|
||||
return oldRouteId !== routeNew.id && oldRouteId.indexOf(routeNew.name) > -1
|
||||
})
|
||||
},
|
||||
buildTreeData: function(data) {
|
||||
data.forEach(ele => {
|
||||
const parentId = ele.parentId
|
||||
if (parentId === 0) {
|
||||
// 是根元素 ,不做任何操作,如果是正常的for-i循环,可以直接continue.
|
||||
} else {
|
||||
// 如果ele是子元素的话 ,把ele扔到他的父亲的child数组中.
|
||||
data.forEach(d => {
|
||||
if (d.id === parentId) {
|
||||
let childArray = d.children
|
||||
if (!childArray) {
|
||||
childArray = []
|
||||
}
|
||||
childArray.push(ele)
|
||||
d.children = childArray
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// 去除重复元素
|
||||
data = data.filter(ele => ele.parentId === 0)
|
||||
return data
|
||||
},
|
||||
onSearchTable: function() {
|
||||
this.loadTable()
|
||||
},
|
||||
onDisable: function(row) {
|
||||
this.confirm(`确定要禁用 ${row.serviceId}(${row.ipPort}) 吗?`, function(done) {
|
||||
this.post('service.instance.offline', row, function() {
|
||||
this.tip('禁用成功')
|
||||
done()
|
||||
this.loadTableDelay()
|
||||
})
|
||||
})
|
||||
},
|
||||
onEnable: function(row) {
|
||||
this.confirm(`确定要启用 ${row.serviceId}(${row.ipPort}) 吗?`, function(done) {
|
||||
this.post('service.instance.online', row, function() {
|
||||
this.tip('启用成功')
|
||||
done()
|
||||
this.loadTableDelay()
|
||||
})
|
||||
})
|
||||
},
|
||||
doEnvOnline: function(row, callback) {
|
||||
this.post('service.instance.env.online', row, function() {
|
||||
callback && callback.call(this)
|
||||
})
|
||||
},
|
||||
onEnvPreOpen: function(row) {
|
||||
this.confirm(`确定要开启 ${row.serviceId}(${row.ipPort}) 预发布吗?`, function(done) {
|
||||
this.post('service.instance.env.pre.open', row, function() {
|
||||
this.tip('预发布成功')
|
||||
done()
|
||||
this.loadTableDelay()
|
||||
})
|
||||
})
|
||||
},
|
||||
onEnvPreClose: function(row) {
|
||||
this.confirm(`确定要结束 ${row.serviceId}(${row.ipPort}) 预发布吗?`, function(done) {
|
||||
this.doEnvOnline(row, function() {
|
||||
this.tip('操作成功')
|
||||
done()
|
||||
this.loadTableDelay()
|
||||
})
|
||||
})
|
||||
},
|
||||
onEnvGrayOpen: function(row) {
|
||||
this.confirm(`确定要开启 ${row.serviceId}(${row.ipPort}) 灰度吗?`, function(done) {
|
||||
this.post('service.instance.env.gray.open', row, function() {
|
||||
this.tip('开启成功')
|
||||
done()
|
||||
this.loadTableDelay()
|
||||
})
|
||||
})
|
||||
},
|
||||
onEnvGrayClose: function(row) {
|
||||
this.confirm(`确定要结束 ${row.serviceId}(${row.ipPort}) 灰度吗?`, function(done) {
|
||||
this.doEnvOnline(row, function() {
|
||||
this.tip('操作成功')
|
||||
done()
|
||||
this.loadTableDelay()
|
||||
})
|
||||
})
|
||||
},
|
||||
onGrayConfigUpdate: function(row) {
|
||||
const serviceId = row.serviceId
|
||||
this.loadRouteList(serviceId)
|
||||
this.post('service.gray.config.get', { serviceId: serviceId }, function(resp) {
|
||||
this.grayDialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
const data = resp.data
|
||||
Object.assign(this.grayForm, {
|
||||
serviceId: serviceId,
|
||||
userKeyContent: data.userKeyContent || '',
|
||||
grayRouteConfigList: this.createGrayRouteConfigList(data.nameVersionContent)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
onGrayConfigSave: function() {
|
||||
this.$refs.grayForm.validate((valid) => {
|
||||
if (valid) {
|
||||
const nameVersionContents = []
|
||||
const grayRouteConfigList = this.grayForm.grayRouteConfigList
|
||||
for (let i = 0; i < grayRouteConfigList.length; i++) {
|
||||
const config = grayRouteConfigList[i]
|
||||
nameVersionContents.push(config.oldRouteId + '=' + config.newVersion)
|
||||
}
|
||||
this.grayForm.nameVersionContent = nameVersionContents.join(',')
|
||||
this.post('service.gray.config.save', this.grayForm, function() {
|
||||
this.grayDialogVisible = false
|
||||
this.tip('保存成功')
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
createGrayRouteConfigList: function(nameVersionContent) {
|
||||
if (!nameVersionContent) {
|
||||
return [{
|
||||
oldRouteId: '',
|
||||
newVersion: '',
|
||||
key: Date.now()
|
||||
}]
|
||||
}
|
||||
const list = []
|
||||
const arr = nameVersionContent.split(',')
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const el = arr[i]
|
||||
const elArr = el.split('=')
|
||||
list.push({
|
||||
oldRouteId: elArr[0],
|
||||
newVersion: elArr[1],
|
||||
key: Date.now()
|
||||
})
|
||||
}
|
||||
return list
|
||||
},
|
||||
onChangeOldRoute: function(config) {
|
||||
config.newVersion = ''
|
||||
},
|
||||
addNameVersion: function() {
|
||||
this.grayForm.grayRouteConfigList.push({
|
||||
oldRouteId: '',
|
||||
newVersion: '',
|
||||
key: Date.now()
|
||||
})
|
||||
},
|
||||
removeNameVersion: function(item) {
|
||||
const index = this.grayForm.grayRouteConfigList.indexOf(item)
|
||||
if (index !== -1) {
|
||||
this.grayForm.grayRouteConfigList.splice(index, 1)
|
||||
}
|
||||
},
|
||||
renderServiceName: function(row) {
|
||||
let instanceCount = ''
|
||||
// 如果是父节点
|
||||
if (row.parentId === 0) {
|
||||
const children = row.children || []
|
||||
const childCount = children.length
|
||||
const onlineCount = children.filter(el => {
|
||||
return el.status === 'UP'
|
||||
}).length
|
||||
instanceCount = `(${onlineCount}/${childCount})`
|
||||
}
|
||||
return row.serviceId + instanceCount
|
||||
},
|
||||
loadTableDelay: function() {
|
||||
const that = this
|
||||
setTimeout(function() {
|
||||
that.loadTable()
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|