use vue-element-admin

This commit is contained in:
hubery.wang 2020-12-04 01:08:40 +08:00
parent 3a1ee26967
commit 54673e972b
42 changed files with 1946 additions and 164 deletions

View File

@ -13,6 +13,7 @@
"echarts": "^4.9.0", "echarts": "^4.9.0",
"element-ui": "^2.14.1", "element-ui": "^2.14.1",
"humanize-plus": "^1.8.2", "humanize-plus": "^1.8.2",
"js-cookie": "^2.2.1",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-router": "^3.4.9", "vue-router": "^3.4.9",
"vuex": "^3.5.1", "vuex": "^3.5.1",
@ -36,6 +37,7 @@
"less-loader": "^7.1.0", "less-loader": "^7.1.0",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"sass-loader": "^10.1.0", "sass-loader": "^10.1.0",
"svg-sprite-loader": "^5.0.0",
"vue-template-compiler": "^2.6.12" "vue-template-compiler": "^2.6.12"
}, },
"browserslist": [ "browserslist": [

View File

@ -1,87 +1,5 @@
<template> <template>
<div id="app"> <div id="app">
<header class="grid-content header-color"> <router-view />
<el-row>
<a class="brand" href="#">frp</a>
</el-row>
</header>
<section>
<el-row>
<el-col id="side-nav" :xs="24" :md="4">
<el-menu default-active="1" mode="vertical" theme="light" router @select="handleSelect">
<el-menu-item index="/">Overview</el-menu-item>
<el-submenu index="/proxies">
<template slot="title">Proxies</template>
<el-menu-item index="/proxies/tcp">TCP</el-menu-item>
<el-menu-item index="/proxies/udp">UDP</el-menu-item>
<el-menu-item index="/proxies/http">HTTP</el-menu-item>
<el-menu-item index="/proxies/https">HTTPS</el-menu-item>
<el-menu-item index="/proxies/stcp">STCP</el-menu-item>
</el-submenu>
<el-menu-item index="">Help</el-menu-item>
</el-menu>
</el-col>
<el-col :xs="24" :md="20">
<div id="content">
<router-view v-if="serverInfo" />
</div>
</el-col>
</el-row>
</section>
</div> </div>
</template> </template>
<script>
export default {
computed: {
serverInfo() {
return this.$store.state.serverInfo
}
},
async created() {
this.$store.dispatch('fetchServerInfo')
},
methods: {
handleSelect(key, path) {
if (key === '') {
window.open('https://github.com/fatedier/frp')
}
}
}
}
</script>
<style>
body {
background-color: #fafafa;
margin: 0px;
font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, sans-serif;
}
header {
width: 100%;
height: 60px;
}
.header-color {
background: #58b7ff;
}
#content {
margin-top: 20px;
padding-right: 40px;
}
.brand {
color: #fff;
background-color: transparent;
margin-left: 20px;
float: left;
line-height: 25px;
font-size: 25px;
padding: 15px 15px;
height: 30px;
text-decoration: none;
}
</style>

View File

@ -0,0 +1,40 @@
<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.path
}
}
}
</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>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<div class="navbar">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb class="breadcrumb-container" />
<!-- <div class="right-menu"></div> -->
</div>
</template>
<script>
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
export default {
components: {
Breadcrumb,
Hamburger
},
computed: {
sidebar() {
return this.$store.state.app.sidebar
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
}
}
}
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.breadcrumb-container {
float: left;
}
}
</style>

View File

@ -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)
}
}
}
}
}

View File

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

View File

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

View File

@ -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 src="/favicon.ico" alt="" class="sidebar-logo">
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img src="/favicon.ico" alt="" class="sidebar-logo">
<h1 class="sidebar-title">frps</h1>
</router-link>
</transition>
</div>
</template>
<script>
export default {
name: 'SidebarLogo',
props: {
collapse: {
type: Boolean,
required: true
}
},
computed: {
baseUrl() {
return process.env.BASE_URL
}
}
}
</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;
overflow: hidden;
& .sidebar-logo-link {
height: 100%;
width: 100%;
& .sidebar-logo {
margin-right: 16px;
height: 36px;
width: 36px;
object-fit: contain;
color: white;
vertical-align: middle;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 18px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
width: 120px;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px !important;
}
}
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div v-if="!item.hidden">
<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>

View File

@ -0,0 +1,53 @@
<template>
<div class="has-logo">
<logo :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"
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 Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
import { routes } from '@/router'
export default {
components: { SidebarItem, Logo },
computed: {
routes() {
return routes
},
sidebar() {
return this.$store.state.app.sidebar
},
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
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>

View File

@ -0,0 +1,3 @@
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
export { default as AppMain } from './AppMain'

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

View File

@ -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 })
}
}
}
}
}

View File

@ -0,0 +1,65 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="index">
<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
const matched = this.$route.matched.filter(item => item.meta && item.meta.title)
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
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>

View File

@ -0,0 +1,40 @@
<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() {
// eslint-disable-next-line vue/custom-event-name-casing
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :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: {
isExternal () {
return /^(https?:|mailto:|tel:)/.test(this.iconClass)
},
iconName () {
return `#icon-${this.iconClass}`
},
svgClass () {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon () {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>

View File

@ -0,0 +1,9 @@
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon' // svg component
// register globally
Vue.component('SvgIcon', SvgIcon)
const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)

View 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

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1607014980188" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3560" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><defs><style type="text/css"></style></defs><path d="M512 85.333333a426.666667 426.666667 0 1 0 426.666667 426.666667A426.666667 426.666667 0 0 0 512 85.333333z m42.666667 661.333334a21.333333 21.333333 0 0 1-21.333334 21.333333h-42.666666a21.333333 21.333333 0 0 1-21.333334-21.333333v-42.666667a21.333333 21.333333 0 0 1 21.333334-21.333333h42.666666a21.333333 21.333333 0 0 1 21.333334 21.333333z m122.88-338.773334a123.306667 123.306667 0 0 1-85.333334 116.48l-37.546666 13.226667a5.546667 5.546667 0 0 0-3.413334 5.12v33.28a21.333333 21.333333 0 0 1-21.333333 21.333333h-32.426667a21.333333 21.333333 0 0 1-21.333333-21.333333v-33.28a80.213333 80.213333 0 0 1 55.04-75.946667l40.533333-13.226666a48.213333 48.213333 0 0 0 32.426667-45.653334V384A47.786667 47.786667 0 0 0 554.666667 336.213333h-85.333334A47.786667 47.786667 0 0 0 421.546667 384v21.333333a21.333333 21.333333 0 0 1-21.333334 21.333334h-32.426666a21.333333 21.333333 0 0 1-21.333334-21.333334V384A122.88 122.88 0 0 1 469.333333 261.12h85.333334A122.88 122.88 0 0 1 677.546667 384z" p-id="3561"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

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

View File

@ -1,10 +1,11 @@
import Vue from 'vue' import Vue from 'vue'
// import ElementUI from 'element-ui' import ElementUI from 'element-ui'
import { Button, Form, FormItem, Row, Col, Table, TableColumn, Popover, Menu, Submenu, MenuItem, Tag, Message } from 'element-ui'
import lang from 'element-ui/lib/locale/lang/en' import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale' import locale from 'element-ui/lib/locale'
import 'element-ui/lib/theme-chalk/index.css' import 'element-ui/lib/theme-chalk/index.css'
import './utils/less/custom.less' import './utils/less/custom.less'
import '@/icons'
import '@/styles/index.scss'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
@ -13,19 +14,7 @@ import 'whatwg-fetch'
locale.use(lang) locale.use(lang)
Vue.use(Button) Vue.use(ElementUI)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Row)
Vue.use(Col)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Popover)
Vue.use(Menu)
Vue.use(Submenu)
Vue.use(MenuItem)
Vue.use(Tag)
Vue.prototype.$message = Message
import fetch from '@/utils/fetch' import fetch from '@/utils/fetch'
Vue.prototype.$fetch = fetch Vue.prototype.$fetch = fetch

View File

@ -1,45 +1,91 @@
import Vue from 'vue' import Vue from 'vue'
import Router from 'vue-router' import Router from 'vue-router'
import Overview from '../components/Overview.vue' import AdminLayout from '@/components/AdminLayout'
import ProxiesTcp from '../components/ProxiesTcp.vue'
import ProxiesUdp from '../components/ProxiesUdp.vue'
import ProxiesHttp from '../components/ProxiesHttp.vue'
import ProxiesHttps from '../components/ProxiesHttps.vue'
import ProxiesStcp from '../components/ProxiesStcp.vue'
Vue.use(Router) Vue.use(Router)
export default new Router({ export const routes = [
routes: [
{ {
path: '/', path: '/',
component: AdminLayout,
meta: {
icon: 'dashboard'
},
children: [
{
path: '/',
component: () => import('@/views/Overview'),
name: 'Overview', name: 'Overview',
component: Overview meta: {
}, title: 'Overview'
{ }
path: '/proxies/tcp',
name: 'ProxiesTcp',
component: ProxiesTcp
},
{
path: '/proxies/udp',
name: 'ProxiesUdp',
component: ProxiesUdp
},
{
path: '/proxies/http',
name: 'ProxiesHttp',
component: ProxiesHttp
},
{
path: '/proxies/https',
name: 'ProxiesHttps',
component: ProxiesHttps
},
{
path: '/proxies/stcp',
name: 'ProxiesStcp',
component: ProxiesStcp
} }
] ]
}) },
{
path: '/proxies',
component: AdminLayout,
meta: {
title: 'Proxies',
icon: 'proxy'
},
children: [
{
path: 'tcp',
component: () => import('@/views/ProxiesTcp'),
name: 'ProxiesTcp',
meta: {
title: 'TCP'
}
},
{
path: 'udp',
component: () => import('@/views/ProxiesUdp'),
name: 'ProxiesUdp',
meta: {
title: 'UDP'
}
},
{
path: 'http',
component: () => import('@/views/ProxiesHttp'),
name: 'ProxiesHttp',
meta: {
title: 'HTTP'
}
},
{
path: 'https',
component: () => import('@/views/ProxiesHttps'),
name: 'ProxiesHttps',
meta: {
title: 'HTTPS'
}
},
{
path: 'stcp',
component: () => import('@/views/ProxiesStcp'),
name: 'ProxiesStcp',
meta: {
title: 'STCP'
}
}
]
},
{
path: 'help',
component: AdminLayout,
children: [
{
path: 'https://github.com/fatedier/frp',
component: () => import('@/views/Overview'),
meta: {
title: 'Help',
icon: 'help'
}
}
]
}
]
export default new Router({ routes })

View File

@ -1,24 +1,19 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import fetch from '@/utils/fetch'
Vue.use(Vuex) Vue.use(Vuex)
const modulesFiles = require.context('./modules', true, /\.js$/)
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {})
const store = new Vuex.Store({ const store = new Vuex.Store({
state: { modules
serverInfo: null
},
mutations: {
SET_SERVER_INFO(state, serverInfo) {
state.serverInfo = serverInfo
}
},
actions: {
async fetchServerInfo({ commit }) {
const json = await fetch('serverinfo')
commit('SET_SERVER_INFO', json || null)
return json
}
}
}) })
export default store export default store

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

View File

@ -0,0 +1,29 @@
import Vue from 'vue'
import Vuex from 'vuex'
import fetch from '@/utils/fetch'
Vue.use(Vuex)
const state = {
serverInfo: null
}
const mutations = {
SET_SERVER_INFO(state, serverInfo) {
state.serverInfo = serverInfo
}
}
const actions = {
async fetchServerInfo({ commit }) {
const json = await fetch('serverinfo')
commit('SET_SERVER_INFO', json || null)
return json
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -0,0 +1,49 @@
// 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
}
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}

View File

@ -0,0 +1,68 @@
@import "./variables.scss";
@import "./mixin.scss";
@import "./transition.scss";
@import "./element-ui.scss";
@import "./sidebar.scss";
@import "./layout.scss";
body {
height: 100%;
margin: 0;
-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;
}

View File

@ -0,0 +1,525 @@
/*
* @Description: 布局来源ColorUI
* @Author: wwh
* @Date: 2020-07-12 22:36:59
* @LastEditTime: 2020-07-12 23:04:49
* @LastEditors: wwh
* @FilePath: \web\src\styles\layout.scss
*/
/* -- flex弹性布局 -- */
.flex {
display: flex;
}
.basis-xs {
flex-basis: 20%;
}
.basis-sm {
flex-basis: 40%;
}
.basis-df {
flex-basis: 50%;
}
.basis-lg {
flex-basis: 60%;
}
.basis-xl {
flex-basis: 80%;
}
.flex-sub {
flex: 1;
}
.flex-twice {
flex: 2;
}
.flex-treble {
flex: 3;
}
.flex-direction {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.align-start {
align-items: flex-start;
}
.align-end {
align-items: flex-end;
}
.align-center {
align-items: center;
}
.align-stretch {
align-items: stretch;
}
.self-start {
align-self: flex-start;
}
.self-center {
align-self: flex-center;
}
.self-end {
align-self: flex-end;
}
.self-stretch {
align-self: stretch;
}
.align-stretch {
align-items: stretch;
}
.justify-start {
justify-content: flex-start;
}
.justify-end {
justify-content: flex-end;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
/* grid布局 */
.grid {
display: flex;
flex-wrap: wrap;
}
.grid.grid-square {
overflow: hidden;
}
.grid.grid-square .cu-tag {
position: absolute;
right: 0;
top: 0;
border-bottom-left-radius: 6px;
padding: 6px 12px;
height: auto;
background-color: rgba(0, 0, 0, 0.5);
}
.grid.grid-square > view > text[class*="cuIcon-"] {
font-size: 52px;
position: absolute;
color: #8799a3;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.grid.grid-square > view {
margin-right: 20px;
margin-bottom: 20px;
border-radius: 6px;
position: relative;
overflow: hidden;
}
.grid.grid-square > view.bg-img image {
width: 100%;
height: 100%;
position: absolute;
}
.grid.col-1.grid-square > view {
padding-bottom: 100%;
height: 0;
margin-right: 0;
}
.grid.col-2.grid-square > view {
padding-bottom: calc((100% - 20px) / 2);
height: 0;
width: calc((100% - 20px) / 2);
}
.grid.col-3.grid-square > view {
padding-bottom: calc((100% - 40px) / 3);
height: 0;
width: calc((100% - 40px) / 3);
}
.grid.col-4.grid-square > view {
padding-bottom: calc((100% - 60px) / 4);
height: 0;
width: calc((100% - 60px) / 4);
}
.grid.col-5.grid-square > view {
padding-bottom: calc((100% - 80px) / 5);
height: 0;
width: calc((100% - 80px) / 5);
}
.grid.col-2.grid-square > view:nth-child(2n),
.grid.col-3.grid-square > view:nth-child(3n),
.grid.col-4.grid-square > view:nth-child(4n),
.grid.col-5.grid-square > view:nth-child(5n) {
margin-right: 0;
}
.grid.col-1 > view {
width: 100%;
}
.grid.col-2 > view {
width: 50%;
}
.grid.col-3 > view {
width: 33.33%;
}
.grid.col-4 > view {
width: 25%;
}
.grid.col-5 > view {
width: 20%;
}
/* -- 内外边距 -- */
.margin-0 {
margin: 0;
}
.margin-xs {
margin: 10px;
}
.margin-sm {
margin: 20px;
}
.margin {
margin: 30px;
}
.margin-lg {
margin: 40px;
}
.margin-xl {
margin: 50px;
}
.margin-top-xs {
margin-top: 10px;
}
.margin-top-sm {
margin-top: 20px;
}
.margin-top {
margin-top: 30px;
}
.margin-top-lg {
margin-top: 40px;
}
.margin-top-xl {
margin-top: 50px;
}
.margin-right-xs {
margin-right: 10px;
}
.margin-right-sm {
margin-right: 20px;
}
.margin-right {
margin-right: 30px;
}
.margin-right-lg {
margin-right: 40px;
}
.margin-right-xl {
margin-right: 50px;
}
.margin-bottom-xs {
margin-bottom: 10px;
}
.margin-bottom-sm {
margin-bottom: 20px;
}
.margin-bottom {
margin-bottom: 30px;
}
.margin-bottom-lg {
margin-bottom: 40px;
}
.margin-bottom-xl {
margin-bottom: 50px;
}
.margin-left-xs {
margin-left: 10px;
}
.margin-left-sm {
margin-left: 20px;
}
.margin-left {
margin-left: 30px;
}
.margin-left-lg {
margin-left: 40px;
}
.margin-left-xl {
margin-left: 50px;
}
.margin-lr-xs {
margin-left: 10px;
margin-right: 10px;
}
.margin-lr-sm {
margin-left: 20px;
margin-right: 20px;
}
.margin-lr {
margin-left: 30px;
margin-right: 30px;
}
.margin-lr-lg {
margin-left: 40px;
margin-right: 40px;
}
.margin-lr-xl {
margin-left: 50px;
margin-right: 50px;
}
.margin-tb-xs {
margin-top: 10px;
margin-bottom: 10px;
}
.margin-tb-sm {
margin-top: 20px;
margin-bottom: 20px;
}
.margin-tb {
margin-top: 30px;
margin-bottom: 30px;
}
.margin-tb-lg {
margin-top: 40px;
margin-bottom: 40px;
}
.margin-tb-xl {
margin-top: 50px;
margin-bottom: 50px;
}
.padding-0 {
padding: 0;
}
.padding-xs {
padding: 10px;
}
.padding-sm {
padding: 20px;
}
.padding {
padding: 30px;
}
.padding-lg {
padding: 40px;
}
.padding-xl {
padding: 50px;
}
.padding-top-xs {
padding-top: 10px;
}
.padding-top-sm {
padding-top: 20px;
}
.padding-top {
padding-top: 30px;
}
.padding-top-lg {
padding-top: 40px;
}
.padding-top-xl {
padding-top: 50px;
}
.padding-right-xs {
padding-right: 10px;
}
.padding-right-sm {
padding-right: 20px;
}
.padding-right {
padding-right: 30px;
}
.padding-right-lg {
padding-right: 40px;
}
.padding-right-xl {
padding-right: 50px;
}
.padding-bottom-xs {
padding-bottom: 10px;
}
.padding-bottom-sm {
padding-bottom: 20px;
}
.padding-bottom {
padding-bottom: 30px;
}
.padding-bottom-lg {
padding-bottom: 40px;
}
.padding-bottom-xl {
padding-bottom: 50px;
}
.padding-left-xs {
padding-left: 10px;
}
.padding-left-sm {
padding-left: 20px;
}
.padding-left {
padding-left: 30px;
}
.padding-left-lg {
padding-left: 40px;
}
.padding-left-xl {
padding-left: 50px;
}
.padding-lr-xs {
padding-left: 10px;
padding-right: 10px;
}
.padding-lr-sm {
padding-left: 20px;
padding-right: 20px;
}
.padding-lr {
padding-left: 30px;
padding-right: 30px;
}
.padding-lr-lg {
padding-left: 40px;
padding-right: 40px;
}
.padding-lr-xl {
padding-left: 50px;
padding-right: 50px;
}
.padding-tb-xs {
padding-top: 10px;
padding-bottom: 10px;
}
.padding-tb-sm {
padding-top: 20px;
padding-bottom: 20px;
}
.padding-tb {
padding-top: 30px;
padding-bottom: 30px;
}
.padding-tb-lg {
padding-top: 40px;
padding-bottom: 40px;
}
.padding-tb-xl {
padding-top: 50px;
padding-bottom: 50px;
}

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

View File

@ -0,0 +1,209 @@
#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;
}
.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: 12px;
}
}
.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;
}
}
}

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

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

View File

@ -0,0 +1,7 @@
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}

View File

@ -72,15 +72,15 @@ export default {
}, },
computed: { computed: {
serverInfo() { serverInfo() {
return this.$store.state.serverInfo return this.$store.state.server.serverInfo
} }
}, },
mounted() { async mounted() {
await this.$store.dispatch('server/fetchServerInfo')
this.initData() this.initData()
}, },
methods: { methods: {
initData() { initData() {
console.log(!!this.serverInfo, this.serverInfo)
if (!this.serverInfo) return if (!this.serverInfo) return
this.version = this.serverInfo.version this.version = this.serverInfo.version

View File

@ -60,7 +60,7 @@
<script> <script>
import Humanize from 'humanize-plus' import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from '@/components/Traffic.vue'
import { HttpProxy } from '../utils/proxy.js' import { HttpProxy } from '../utils/proxy.js'
export default { export default {
components: { components: {
@ -75,10 +75,11 @@ export default {
}, },
computed: { computed: {
serverInfo() { serverInfo() {
return this.$store.state.serverInfo return this.$store.state.server.serverInfo
} }
}, },
mounted() { async mounted() {
await this.$store.dispatch('server/fetchServerInfo')
this.initData() this.initData()
}, },
methods: { methods: {

View File

@ -54,7 +54,7 @@
<script> <script>
import Humanize from 'humanize-plus' import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from '@/components/Traffic.vue'
import { HttpsProxy } from '../utils/proxy.js' import { HttpsProxy } from '../utils/proxy.js'
export default { export default {
components: { components: {
@ -69,10 +69,11 @@ export default {
}, },
computed: { computed: {
serverInfo() { serverInfo() {
return this.$store.state.serverInfo return this.$store.state.server.serverInfo
} }
}, },
mounted() { async mounted() {
await this.$store.dispatch('server/fetchServerInfo')
this.initData() this.initData()
}, },
methods: { methods: {

View File

@ -49,7 +49,7 @@
<script> <script>
import Humanize from 'humanize-plus' import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from '@/components/Traffic.vue'
import { StcpProxy } from '../utils/proxy.js' import { StcpProxy } from '../utils/proxy.js'
export default { export default {
components: { components: {

View File

@ -53,7 +53,7 @@
<script> <script>
import Humanize from 'humanize-plus' import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from '@/components/Traffic.vue'
import { TcpProxy } from '../utils/proxy.js' import { TcpProxy } from '../utils/proxy.js'
export default { export default {
components: { components: {

View File

@ -51,7 +51,7 @@
<script> <script>
import Humanize from 'humanize-plus' import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from '@/components/Traffic.vue'
import { UdpProxy } from '../utils/proxy.js' import { UdpProxy } from '../utils/proxy.js'
export default { export default {
components: { components: {

View File

@ -0,0 +1,7 @@
<script>
export default {
created() {
}
}
</script>

View File

@ -1,3 +1,9 @@
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = { module.exports = {
publicPath: './', publicPath: './',
devServer: { devServer: {
@ -12,5 +18,28 @@ module.exports = {
} }
} }
} }
},
chainWebpack(config) {
config.plugins.delete('preload') // TODO: need test
config.plugins.delete('prefetch') // TODO: need test
// set svg-sprite-loader
config.module
.rule('svg')
.exclude.add(resolve('src/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
config.when(process.env.NODE_ENV === 'development', config => config.devtool('eval-source-map'))
} }
} }