use vue-element-admin
This commit is contained in:
parent
3a1ee26967
commit
54673e972b
@ -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": [
|
||||||
|
@ -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>
|
|
||||||
|
40
web/frps/src/components/AdminLayout/components/AppMain.vue
Normal file
40
web/frps/src/components/AdminLayout/components/AppMain.vue
Normal 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>
|
57
web/frps/src/components/AdminLayout/components/Navbar.vue
Normal file
57
web/frps/src/components/AdminLayout/components/Navbar.vue
Normal 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>
|
@ -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 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>
|
@ -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>
|
@ -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>
|
3
web/frps/src/components/AdminLayout/components/index.js
Normal file
3
web/frps/src/components/AdminLayout/components/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as Navbar } from './Navbar'
|
||||||
|
export { default as Sidebar } from './Sidebar'
|
||||||
|
export { default as AppMain } from './AppMain'
|
93
web/frps/src/components/AdminLayout/index.vue
Normal file
93
web/frps/src/components/AdminLayout/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>
|
||||||
|
<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>
|
45
web/frps/src/components/AdminLayout/mixin/ResizeHandler.js
Normal file
45
web/frps/src/components/AdminLayout/mixin/ResizeHandler.js
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
web/frps/src/components/Breadcrumb/index.vue
Normal file
65
web/frps/src/components/Breadcrumb/index.vue
Normal 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>
|
40
web/frps/src/components/Hamburger/index.vue
Normal file
40
web/frps/src/components/Hamburger/index.vue
Normal 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>
|
59
web/frps/src/components/SvgIcon/index.vue
Normal file
59
web/frps/src/components/SvgIcon/index.vue
Normal 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>
|
9
web/frps/src/icons/index.js
Normal file
9
web/frps/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('SvgIcon', SvgIcon)
|
||||||
|
|
||||||
|
const req = require.context('./svg', false, /\.svg$/)
|
||||||
|
const requireAll = requireContext => requireContext.keys().map(requireContext)
|
||||||
|
requireAll(req)
|
1
web/frps/src/icons/svg/dashboard.svg
Normal file
1
web/frps/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
web/frps/src/icons/svg/help.svg
Normal file
1
web/frps/src/icons/svg/help.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
1
web/frps/src/icons/svg/proxy.svg
Normal file
1
web/frps/src/icons/svg/proxy.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.7 KiB |
22
web/frps/src/icons/svgo.yml
Normal file
22
web/frps/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'
|
@ -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
|
||||||
|
@ -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,
|
||||||
name: 'Overview',
|
meta: {
|
||||||
component: Overview
|
icon: 'dashboard'
|
||||||
},
|
},
|
||||||
{
|
children: [
|
||||||
path: '/proxies/tcp',
|
{
|
||||||
name: 'ProxiesTcp',
|
path: '/',
|
||||||
component: ProxiesTcp
|
component: () => import('@/views/Overview'),
|
||||||
|
name: 'Overview',
|
||||||
|
meta: {
|
||||||
|
title: 'Overview'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/proxies',
|
||||||
|
component: AdminLayout,
|
||||||
|
meta: {
|
||||||
|
title: 'Proxies',
|
||||||
|
icon: 'proxy'
|
||||||
},
|
},
|
||||||
{
|
children: [
|
||||||
path: '/proxies/udp',
|
{
|
||||||
name: 'ProxiesUdp',
|
path: 'tcp',
|
||||||
component: ProxiesUdp
|
component: () => import('@/views/ProxiesTcp'),
|
||||||
},
|
name: 'ProxiesTcp',
|
||||||
{
|
meta: {
|
||||||
path: '/proxies/http',
|
title: 'TCP'
|
||||||
name: 'ProxiesHttp',
|
}
|
||||||
component: ProxiesHttp
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'udp',
|
||||||
path: '/proxies/https',
|
component: () => import('@/views/ProxiesUdp'),
|
||||||
name: 'ProxiesHttps',
|
name: 'ProxiesUdp',
|
||||||
component: ProxiesHttps
|
meta: {
|
||||||
},
|
title: 'UDP'
|
||||||
{
|
}
|
||||||
path: '/proxies/stcp',
|
},
|
||||||
name: 'ProxiesStcp',
|
{
|
||||||
component: ProxiesStcp
|
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 })
|
||||||
|
@ -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
|
||||||
|
48
web/frps/src/store/modules/app.js
Normal file
48
web/frps/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
|
||||||
|
}
|
29
web/frps/src/store/modules/server.js
Normal file
29
web/frps/src/store/modules/server.js
Normal 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
|
||||||
|
}
|
49
web/frps/src/styles/element-ui.scss
Normal file
49
web/frps/src/styles/element-ui.scss
Normal 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;
|
||||||
|
}
|
68
web/frps/src/styles/index.scss
Normal file
68
web/frps/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";
|
||||||
|
@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;
|
||||||
|
}
|
525
web/frps/src/styles/layout.scss
Normal file
525
web/frps/src/styles/layout.scss
Normal 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;
|
||||||
|
}
|
28
web/frps/src/styles/mixin.scss
Normal file
28
web/frps/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%;
|
||||||
|
}
|
209
web/frps/src/styles/sidebar.scss
Normal file
209
web/frps/src/styles/sidebar.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
web/frps/src/styles/transition.scss
Normal file
48
web/frps/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
web/frps/src/styles/variables.scss
Normal file
25
web/frps/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;
|
||||||
|
}
|
7
web/frps/src/utils/validate.js
Normal file
7
web/frps/src/utils/validate.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function isExternal(path) {
|
||||||
|
return /^(https?:|mailto:|tel:)/.test(path)
|
||||||
|
}
|
@ -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
|
@ -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: {
|
@ -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: {
|
@ -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: {
|
@ -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: {
|
@ -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: {
|
7
web/frps/src/views/index.vue
Normal file
7
web/frps/src/views/index.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
created() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -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'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user