diff --git a/web/frps/package.json b/web/frps/package.json index cb78bbdf..f7d41e83 100644 --- a/web/frps/package.json +++ b/web/frps/package.json @@ -13,6 +13,7 @@ "echarts": "^4.9.0", "element-ui": "^2.14.1", "humanize-plus": "^1.8.2", + "js-cookie": "^2.2.1", "vue": "^2.6.12", "vue-router": "^3.4.9", "vuex": "^3.5.1", @@ -36,6 +37,7 @@ "less-loader": "^7.1.0", "node-sass": "^5.0.0", "sass-loader": "^10.1.0", + "svg-sprite-loader": "^5.0.0", "vue-template-compiler": "^2.6.12" }, "browserslist": [ diff --git a/web/frps/src/App.vue b/web/frps/src/App.vue index 3ebd41f9..d3e2cd89 100644 --- a/web/frps/src/App.vue +++ b/web/frps/src/App.vue @@ -1,87 +1,5 @@ - - - - diff --git a/web/frps/src/components/AdminLayout/components/AppMain.vue b/web/frps/src/components/AdminLayout/components/AppMain.vue new file mode 100644 index 00000000..f6a3286f --- /dev/null +++ b/web/frps/src/components/AdminLayout/components/AppMain.vue @@ -0,0 +1,40 @@ + + + + + + + diff --git a/web/frps/src/components/AdminLayout/components/Navbar.vue b/web/frps/src/components/AdminLayout/components/Navbar.vue new file mode 100644 index 00000000..8c86649a --- /dev/null +++ b/web/frps/src/components/AdminLayout/components/Navbar.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/web/frps/src/components/AdminLayout/components/Sidebar/FixiOSBug.js b/web/frps/src/components/AdminLayout/components/Sidebar/FixiOSBug.js new file mode 100644 index 00000000..bc14856f --- /dev/null +++ b/web/frps/src/components/AdminLayout/components/Sidebar/FixiOSBug.js @@ -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) + } + } + } + } +} diff --git a/web/frps/src/components/AdminLayout/components/Sidebar/Item.vue b/web/frps/src/components/AdminLayout/components/Sidebar/Item.vue new file mode 100644 index 00000000..b515f615 --- /dev/null +++ b/web/frps/src/components/AdminLayout/components/Sidebar/Item.vue @@ -0,0 +1,29 @@ + diff --git a/web/frps/src/components/AdminLayout/components/Sidebar/Link.vue b/web/frps/src/components/AdminLayout/components/Sidebar/Link.vue new file mode 100644 index 00000000..eb4dd107 --- /dev/null +++ b/web/frps/src/components/AdminLayout/components/Sidebar/Link.vue @@ -0,0 +1,36 @@ + + + + diff --git a/web/frps/src/components/AdminLayout/components/Sidebar/Logo.vue b/web/frps/src/components/AdminLayout/components/Sidebar/Logo.vue new file mode 100644 index 00000000..92bd8dc0 --- /dev/null +++ b/web/frps/src/components/AdminLayout/components/Sidebar/Logo.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/web/frps/src/components/AdminLayout/components/Sidebar/SidebarItem.vue b/web/frps/src/components/AdminLayout/components/Sidebar/SidebarItem.vue new file mode 100644 index 00000000..a418c3d7 --- /dev/null +++ b/web/frps/src/components/AdminLayout/components/Sidebar/SidebarItem.vue @@ -0,0 +1,95 @@ + + + diff --git a/web/frps/src/components/AdminLayout/components/Sidebar/index.vue b/web/frps/src/components/AdminLayout/components/Sidebar/index.vue new file mode 100644 index 00000000..c46ec89f --- /dev/null +++ b/web/frps/src/components/AdminLayout/components/Sidebar/index.vue @@ -0,0 +1,53 @@ + + + diff --git a/web/frps/src/components/AdminLayout/components/index.js b/web/frps/src/components/AdminLayout/components/index.js new file mode 100644 index 00000000..97ee3cd1 --- /dev/null +++ b/web/frps/src/components/AdminLayout/components/index.js @@ -0,0 +1,3 @@ +export { default as Navbar } from './Navbar' +export { default as Sidebar } from './Sidebar' +export { default as AppMain } from './AppMain' diff --git a/web/frps/src/components/AdminLayout/index.vue b/web/frps/src/components/AdminLayout/index.vue new file mode 100644 index 00000000..acd641b7 --- /dev/null +++ b/web/frps/src/components/AdminLayout/index.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/web/frps/src/components/AdminLayout/mixin/ResizeHandler.js b/web/frps/src/components/AdminLayout/mixin/ResizeHandler.js new file mode 100644 index 00000000..e8d0df8c --- /dev/null +++ b/web/frps/src/components/AdminLayout/mixin/ResizeHandler.js @@ -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 }) + } + } + } + } +} diff --git a/web/frps/src/components/Breadcrumb/index.vue b/web/frps/src/components/Breadcrumb/index.vue new file mode 100644 index 00000000..50d2d3b0 --- /dev/null +++ b/web/frps/src/components/Breadcrumb/index.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/web/frps/src/components/Hamburger/index.vue b/web/frps/src/components/Hamburger/index.vue new file mode 100644 index 00000000..56a6ad11 --- /dev/null +++ b/web/frps/src/components/Hamburger/index.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/web/frps/src/components/SvgIcon/index.vue b/web/frps/src/components/SvgIcon/index.vue new file mode 100644 index 00000000..41583bbb --- /dev/null +++ b/web/frps/src/components/SvgIcon/index.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/web/frps/src/icons/index.js b/web/frps/src/icons/index.js new file mode 100644 index 00000000..c3859d53 --- /dev/null +++ b/web/frps/src/icons/index.js @@ -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) diff --git a/web/frps/src/icons/svg/dashboard.svg b/web/frps/src/icons/svg/dashboard.svg new file mode 100644 index 00000000..5317d370 --- /dev/null +++ b/web/frps/src/icons/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/frps/src/icons/svg/help.svg b/web/frps/src/icons/svg/help.svg new file mode 100644 index 00000000..52545879 --- /dev/null +++ b/web/frps/src/icons/svg/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/frps/src/icons/svg/proxy.svg b/web/frps/src/icons/svg/proxy.svg new file mode 100644 index 00000000..67129b02 --- /dev/null +++ b/web/frps/src/icons/svg/proxy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/frps/src/icons/svgo.yml b/web/frps/src/icons/svgo.yml new file mode 100644 index 00000000..d11906ae --- /dev/null +++ b/web/frps/src/icons/svgo.yml @@ -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' diff --git a/web/frps/src/main.js b/web/frps/src/main.js index 9fb7bd0a..010a6cd3 100644 --- a/web/frps/src/main.js +++ b/web/frps/src/main.js @@ -1,10 +1,11 @@ import Vue from 'vue' -// import ElementUI from 'element-ui' -import { Button, Form, FormItem, Row, Col, Table, TableColumn, Popover, Menu, Submenu, MenuItem, Tag, Message } from 'element-ui' +import ElementUI from 'element-ui' import lang from 'element-ui/lib/locale/lang/en' import locale from 'element-ui/lib/locale' import 'element-ui/lib/theme-chalk/index.css' import './utils/less/custom.less' +import '@/icons' +import '@/styles/index.scss' import App from './App.vue' import router from './router' @@ -13,19 +14,7 @@ import 'whatwg-fetch' locale.use(lang) -Vue.use(Button) -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 +Vue.use(ElementUI) import fetch from '@/utils/fetch' Vue.prototype.$fetch = fetch diff --git a/web/frps/src/router/index.js b/web/frps/src/router/index.js index ab421a1f..2ea91b00 100644 --- a/web/frps/src/router/index.js +++ b/web/frps/src/router/index.js @@ -1,45 +1,91 @@ import Vue from 'vue' import Router from 'vue-router' -import Overview from '../components/Overview.vue' -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' +import AdminLayout from '@/components/AdminLayout' Vue.use(Router) -export default new Router({ - routes: [ - { - path: '/', - name: 'Overview', - component: Overview +export const routes = [ + { + path: '/', + component: AdminLayout, + meta: { + icon: 'dashboard' }, - { - path: '/proxies/tcp', - name: 'ProxiesTcp', - component: ProxiesTcp + children: [ + { + path: '/', + component: () => import('@/views/Overview'), + name: 'Overview', + meta: { + title: 'Overview' + } + } + ] + }, + { + path: '/proxies', + component: AdminLayout, + meta: { + title: 'Proxies', + icon: 'proxy' }, - { - 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 - } - ] -}) + 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 }) diff --git a/web/frps/src/store/index.js b/web/frps/src/store/index.js index 437c206d..d3680779 100644 --- a/web/frps/src/store/index.js +++ b/web/frps/src/store/index.js @@ -1,24 +1,19 @@ import Vue from 'vue' import Vuex from 'vuex' -import fetch from '@/utils/fetch' + 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({ - state: { - 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 - } - } + modules }) export default store diff --git a/web/frps/src/store/modules/app.js b/web/frps/src/store/modules/app.js new file mode 100644 index 00000000..7ea7e332 --- /dev/null +++ b/web/frps/src/store/modules/app.js @@ -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 +} diff --git a/web/frps/src/store/modules/server.js b/web/frps/src/store/modules/server.js new file mode 100644 index 00000000..6ef4ee7d --- /dev/null +++ b/web/frps/src/store/modules/server.js @@ -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 +} diff --git a/web/frps/src/styles/element-ui.scss b/web/frps/src/styles/element-ui.scss new file mode 100644 index 00000000..00624119 --- /dev/null +++ b/web/frps/src/styles/element-ui.scss @@ -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; +} diff --git a/web/frps/src/styles/index.scss b/web/frps/src/styles/index.scss new file mode 100644 index 00000000..3619ecf3 --- /dev/null +++ b/web/frps/src/styles/index.scss @@ -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; +} diff --git a/web/frps/src/styles/layout.scss b/web/frps/src/styles/layout.scss new file mode 100644 index 00000000..9a2a6cc1 --- /dev/null +++ b/web/frps/src/styles/layout.scss @@ -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; +} diff --git a/web/frps/src/styles/mixin.scss b/web/frps/src/styles/mixin.scss new file mode 100644 index 00000000..36b74bbd --- /dev/null +++ b/web/frps/src/styles/mixin.scss @@ -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%; +} diff --git a/web/frps/src/styles/sidebar.scss b/web/frps/src/styles/sidebar.scss new file mode 100644 index 00000000..f50daf7f --- /dev/null +++ b/web/frps/src/styles/sidebar.scss @@ -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; + } + } +} diff --git a/web/frps/src/styles/transition.scss b/web/frps/src/styles/transition.scss new file mode 100644 index 00000000..4cb27cc8 --- /dev/null +++ b/web/frps/src/styles/transition.scss @@ -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; +} diff --git a/web/frps/src/styles/variables.scss b/web/frps/src/styles/variables.scss new file mode 100644 index 00000000..be557726 --- /dev/null +++ b/web/frps/src/styles/variables.scss @@ -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; +} diff --git a/web/frps/src/utils/validate.js b/web/frps/src/utils/validate.js new file mode 100644 index 00000000..befc09ea --- /dev/null +++ b/web/frps/src/utils/validate.js @@ -0,0 +1,7 @@ +/** + * @param {string} path + * @returns {Boolean} + */ +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} diff --git a/web/frps/src/components/Overview.vue b/web/frps/src/views/Overview.vue similarity index 97% rename from web/frps/src/components/Overview.vue rename to web/frps/src/views/Overview.vue index fc0cc732..c377601f 100644 --- a/web/frps/src/components/Overview.vue +++ b/web/frps/src/views/Overview.vue @@ -72,15 +72,15 @@ export default { }, computed: { serverInfo() { - return this.$store.state.serverInfo + return this.$store.state.server.serverInfo } }, - mounted() { + async mounted() { + await this.$store.dispatch('server/fetchServerInfo') this.initData() }, methods: { initData() { - console.log(!!this.serverInfo, this.serverInfo) if (!this.serverInfo) return this.version = this.serverInfo.version diff --git a/web/frps/src/components/ProxiesHttp.vue b/web/frps/src/views/ProxiesHttp.vue similarity index 95% rename from web/frps/src/components/ProxiesHttp.vue rename to web/frps/src/views/ProxiesHttp.vue index 1a9d44a3..d9a2f98b 100644 --- a/web/frps/src/components/ProxiesHttp.vue +++ b/web/frps/src/views/ProxiesHttp.vue @@ -60,7 +60,7 @@ diff --git a/web/frps/vue.config.js b/web/frps/vue.config.js index e08236a3..c23de08c 100644 --- a/web/frps/vue.config.js +++ b/web/frps/vue.config.js @@ -1,3 +1,9 @@ +const path = require('path') + +function resolve(dir) { + return path.join(__dirname, dir) +} + module.exports = { publicPath: './', 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')) } }