├── src ├── lang │ ├── es.js │ ├── ja.js │ └── index.js ├── assets │ ├── 401_images │ │ └── 401.gif │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ ├── GithubImages │ │ ├── api.PNG │ │ ├── menu.PNG │ │ ├── role.PNG │ │ ├── user.PNG │ │ ├── login.PNG │ │ └── rolePermission.PNG │ ├── backgd-image │ │ └── backimg.jpg │ ├── sidebar-logo │ │ └── webmini.png │ └── custom-theme │ │ └── fonts │ │ ├── element-icons.ttf │ │ └── element-icons.woff ├── App.vue ├── icons │ ├── svg │ │ ├── chart.svg │ │ ├── size.svg │ │ ├── component.svg │ │ ├── guide.svg │ │ ├── link.svg │ │ ├── drag.svg │ │ ├── money.svg │ │ ├── guide-2.svg │ │ ├── search.svg │ │ ├── email.svg │ │ ├── documentation.svg │ │ ├── lock.svg │ │ ├── fullscreen.svg │ │ ├── user.svg │ │ ├── example.svg │ │ ├── excel.svg │ │ ├── like.svg │ │ ├── star.svg │ │ ├── education.svg │ │ ├── message.svg │ │ ├── table.svg │ │ ├── back-top.svg │ │ ├── theme.svg │ │ ├── close-circle-svgrepo-com.svg │ │ ├── tab.svg │ │ ├── arrow-circle-up-svgrepo-com.svg │ │ ├── peoples.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── data-transfer-both-svgrepo-com.svg │ │ ├── check-circle-svgrepo-com.svg │ │ ├── hamburger.svg │ │ ├── edit.svg │ │ ├── list.svg │ │ ├── eye-off.svg │ │ ├── clipboard.svg │ │ ├── check-mark-button-svgrepo-com.svg │ │ ├── icon.svg │ │ ├── tree-table.svg │ │ ├── international.svg │ │ ├── people.svg │ │ ├── wechat.svg │ │ ├── skill.svg │ │ ├── 404.svg │ │ ├── zip.svg │ │ ├── bug.svg │ │ ├── eye.svg │ │ ├── language.svg │ │ ├── eye-on.svg │ │ ├── exit-fullscreen.svg │ │ ├── pdf.svg │ │ ├── tree.svg │ │ ├── upload-svgrepo-com.svg │ │ ├── dashboard.svg │ │ ├── download-svgrepo-com.svg │ │ ├── headscale3-dots.svg │ │ ├── power-up-svgrepo-com.svg │ │ ├── power-down-svgrepo-com.svg │ │ ├── eye-open.svg │ │ ├── shopping.svg │ │ ├── form.svg │ │ ├── qq.svg │ │ ├── network-backup-svgrepo-com.svg │ │ ├── arrow-circle-down-svgrepo-com.svg │ │ └── menu1.svg │ ├── index.js │ └── svgo.yml ├── components │ ├── ImageCropper │ │ └── utils │ │ │ ├── mimes.js │ │ │ ├── data2blob.js │ │ │ └── effectRipple.js │ ├── IconSelect │ │ ├── requireIcons.js │ │ └── index.vue │ ├── Tinymce │ │ ├── toolbar.js │ │ ├── plugins.js │ │ └── dynamicLoadScript.js │ ├── MarkdownEditor │ │ └── default-options.js │ ├── LangSelect │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── SizeSelect │ │ └── index.vue │ ├── GithubCorner │ │ └── index.vue │ ├── YamlEditor │ │ └── index.vue │ ├── Echarts │ │ ├── PieChart.vue │ │ └── BarChart.vue │ ├── JsonEditor │ │ └── index.vue │ ├── Sticky │ │ └── index.vue │ ├── Pagination │ │ └── index.vue │ ├── ErrorLog │ │ └── index.vue │ ├── Breadcrumb │ │ └── index.vue │ └── Share │ │ └── DropdownMenu.vue ├── api │ ├── connect │ │ └── connect.js │ ├── console │ │ ├── acl.js │ │ ├── routes.js │ │ ├── preauthkey.js │ │ └── machines.js │ ├── system │ │ ├── headscale.js │ │ ├── base.js │ │ ├── api.js │ │ ├── user.js │ │ ├── menu.js │ │ └── role.js │ ├── log │ │ └── operationLog.js │ └── dashboard │ │ └── status.js ├── layout │ ├── components │ │ ├── index.js │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Link.vue │ │ │ ├── Item.vue │ │ │ ├── index.vue │ │ │ └── Logo.vue │ │ ├── AppMain.vue │ │ └── Settings │ │ │ └── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── utils │ ├── get-page-title.js │ ├── auth.js │ ├── event-source.js │ ├── permission.js │ ├── clipboard.js │ ├── error-log.js │ ├── open-window.js │ ├── dataSize.js │ ├── scroll-to.js │ └── validate.js ├── directive │ ├── waves │ │ ├── index.js │ │ ├── waves.css │ │ └── waves.js │ ├── el-drag-dialog │ │ ├── index.js │ │ └── drag.js │ ├── clipboard │ │ ├── index.js │ │ └── clipboard.js │ ├── permission │ │ ├── index.js │ │ └── permission.js │ ├── el-table │ │ ├── index.js │ │ └── adaptive.js │ └── sticky.js ├── views │ ├── redirect │ │ └── index.vue │ ├── profile │ │ └── index.vue │ ├── dashboard │ │ └── components │ │ │ ├── SystemInfoGroup.vue │ │ │ └── mixins │ │ │ └── resize.js │ ├── console │ │ ├── machines │ │ │ └── mixins │ │ │ │ └── data.js │ │ ├── setting │ │ │ └── components │ │ │ │ ├── Card.vue │ │ │ │ ├── mixins │ │ │ │ └── dataFormat.js │ │ │ │ └── CardTab.vue │ │ └── acl │ │ │ └── index.vue │ ├── message │ │ └── system │ │ │ └── index.vue │ └── error-page │ │ └── 401.vue ├── vendor │ ├── publickey.js │ └── Export2Zip.js ├── store │ ├── modules │ │ ├── errorLog.js │ │ ├── settings.js │ │ └── app.js │ ├── getters.js │ └── index.js ├── styles │ ├── variables.scss │ ├── element-variables.scss │ ├── transition.scss │ ├── element-ui.scss │ ├── mixin.scss │ └── btn.scss ├── settings.js ├── main.js └── filters │ └── index.js ├── .eslintignore ├── tests └── unit │ ├── .eslintrc.js │ ├── utils │ ├── param2Obj.spec.js │ ├── formatTime.spec.js │ ├── validate.spec.js │ └── parseTime.spec.js │ └── components │ ├── Hamburger.spec.js │ └── SvgIcon.spec.js ├── postcss.config.js ├── docs ├── images │ ├── role.png │ ├── user.png │ ├── login.png │ ├── dashboard.png │ ├── machine.png │ ├── rolePermission.png │ ├── headscaleconfig.png │ └── ERR_OSSL_EVP_UNSUPPORTED.png └── nginx.md ├── public ├── favicon.ico ├── index.html └── webmini.svg ├── .travis.yml ├── plop-templates ├── utils.js ├── store │ ├── index.hbs │ └── prompt.js ├── view │ ├── index.hbs │ └── prompt.js └── component │ ├── index.hbs │ └── prompt.js ├── .env.staging ├── .env.development ├── .env.production ├── jsconfig.json ├── .editorconfig ├── .gitignore ├── plopfile.js ├── babel.config.js ├── jest.config.js ├── LICENSE └── README.md /src/lang/es.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /src/lang/ja.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/images/role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/docs/images/role.png -------------------------------------------------------------------------------- /docs/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/docs/images/user.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /docs/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/docs/images/login.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /docs/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/docs/images/dashboard.png -------------------------------------------------------------------------------- /docs/images/machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/docs/images/machine.png -------------------------------------------------------------------------------- /plop-templates/utils.js: -------------------------------------------------------------------------------- 1 | exports.notEmpty = name => v => 2 | !v || v.trim() === '' ? `${name} is required` : true 3 | -------------------------------------------------------------------------------- /docs/images/rolePermission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/docs/images/rolePermission.png -------------------------------------------------------------------------------- /src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/404_images/404.png -------------------------------------------------------------------------------- /docs/images/headscaleconfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/docs/images/headscaleconfig.png -------------------------------------------------------------------------------- /src/assets/GithubImages/api.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/GithubImages/api.PNG -------------------------------------------------------------------------------- /src/assets/GithubImages/menu.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/GithubImages/menu.PNG -------------------------------------------------------------------------------- /src/assets/GithubImages/role.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/GithubImages/role.PNG -------------------------------------------------------------------------------- /src/assets/GithubImages/user.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/GithubImages/user.PNG -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/GithubImages/login.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/GithubImages/login.PNG -------------------------------------------------------------------------------- /src/assets/backgd-image/backimg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/backgd-image/backimg.jpg -------------------------------------------------------------------------------- /src/assets/sidebar-logo/webmini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/sidebar-logo/webmini.png -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /docs/images/ERR_OSSL_EVP_UNSUPPORTED.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/docs/images/ERR_OSSL_EVP_UNSUPPORTED.png -------------------------------------------------------------------------------- /src/assets/GithubImages/rolePermission.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/GithubImages/rolePermission.PNG -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/custom-theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QianheYu/headscale-panel-ui/HEAD/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'http://localhost:8088' 6 | VUE_APP_WS_API = 'ws://localhost:8088' 7 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'http://localhost:8088' 6 | VUE_APP_WS_API = 'ws://localhost:8088' 7 | 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/ImageCropper/utils/mimes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'jpg': 'image/jpeg', 3 | 'png': 'image/png', 4 | 'gif': 'image/gif', 5 | 'svg': 'image/svg+xml', 6 | 'psd': 'image/photoshop' 7 | } 8 | -------------------------------------------------------------------------------- /src/api/connect/connect.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function connect(data) { 4 | return request({ 5 | url: '/api/oidc/authorize', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/svg/drag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /plop-templates/store/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if state}} 2 | const state = {} 3 | {{/if}} 4 | 5 | {{#if mutations}} 6 | const mutations = {} 7 | {{/if}} 8 | 9 | {{#if actions}} 10 | const actions = {} 11 | {{/if}} 12 | 13 | export default { 14 | namespaced: true, 15 | {{options}} 16 | } 17 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from './AppMain' 2 | export { default as Navbar } from './Navbar' 3 | export { default as Settings } from './Settings' 4 | export { default as Sidebar } from './Sidebar/index.vue' 5 | export { default as TagsView } from './TagsView/index.vue' 6 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Element Admin' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/directive/waves/index.js: -------------------------------------------------------------------------------- 1 | import waves from './waves' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('waves', waves) 5 | } 6 | 7 | if (window.Vue) { 8 | window.waves = waves 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | waves.install = install 13 | export default waves 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /src/icons/svg/guide-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/IconSelect/requireIcons.js: -------------------------------------------------------------------------------- 1 | 2 | const req = require.context('@/icons/svg', false, /\.svg$/) 3 | const requireAll = requireContext => requireContext.keys() 4 | 5 | const re = /\.\/(.*)\.svg/ 6 | 7 | const icons = requireAll(req).map(i => { 8 | return i.match(re)[1] 9 | }) 10 | 11 | export default icons 12 | -------------------------------------------------------------------------------- /src/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/directive/el-drag-dialog/index.js: -------------------------------------------------------------------------------- 1 | import drag from './drag' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-drag-dialog', drag) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-drag-dialog'] = drag 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | drag.install = install 13 | export default drag 14 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/api/console/acl.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getACL() { 4 | return request({ 5 | url: '/api/console/acl', 6 | method: 'get' 7 | }) 8 | } 9 | export function postACL(data) { 10 | return request({ 11 | url: '/api/console/acl', 12 | method: 'post', 13 | data 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/directive/clipboard/index.js: -------------------------------------------------------------------------------- 1 | import Clipboard from './clipboard' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('Clipboard', Clipboard) 5 | } 6 | 7 | if (window.Vue) { 8 | window.clipboard = Clipboard 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | Clipboard.install = install 13 | export default Clipboard 14 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/directive/permission/index.js: -------------------------------------------------------------------------------- 1 | import permission from './permission' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('permission', permission) 5 | } 6 | 7 | if (window.Vue) { 8 | window['permission'] = permission 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | permission.install = install 13 | export default permission 14 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'headscale-panel-Token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | **/*.log 8 | 9 | tests/**/coverage/ 10 | tests/e2e/reports 11 | selenium-debug.log 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.local 21 | 22 | package-lock.json 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /src/directive/el-table/index.js: -------------------------------------------------------------------------------- 1 | import adaptive from './adaptive' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-height-adaptive-table', adaptive) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-height-adaptive-table'] = adaptive 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | adaptive.install = install 13 | export default adaptive 14 | -------------------------------------------------------------------------------- /src/utils/event-source.js: -------------------------------------------------------------------------------- 1 | import { getToken } from '@/utils/auth' 2 | import { EventSourcePolyfill } from 'event-source-polyfill' 3 | 4 | export function UtilEventSource(resoureUrl, options = {}) { 5 | return new EventSourcePolyfill(resoureUrl, { 6 | headers: { 7 | 'Authorization': 'Bearer ' + getToken() 8 | }, 9 | ...options 10 | }) 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/api/system/headscale.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getHeadscaleConfig() { 4 | return request({ 5 | url: '/api/system/headscale', 6 | method: 'get' 7 | }) 8 | } 9 | 10 | export function postHeadscaleConfig(data) { 11 | return request({ 12 | url: '/api/system/headscale', 13 | method: 'post', 14 | data 15 | }) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/icons/svg/like.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/education.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | const viewGenerator = require('./plop-templates/view/prompt') 2 | const componentGenerator = require('./plop-templates/component/prompt') 3 | const storeGenerator = require('./plop-templates/store/prompt.js') 4 | 5 | module.exports = function(plop) { 6 | plop.setGenerator('view', viewGenerator) 7 | plop.setGenerator('component', componentGenerator) 8 | plop.setGenerator('store', storeGenerator) 9 | } 10 | -------------------------------------------------------------------------------- /src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /plop-templates/view/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if template}} 2 | 5 | {{/if}} 6 | 7 | {{#if script}} 8 | 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /plop-templates/component/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if template}} 2 | 5 | {{/if}} 6 | 7 | {{#if script}} 8 | 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/api/log/operationLog.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取操作日志列表 4 | export function getOperationLogs(params) { 5 | return request({ 6 | url: '/api/log/operation/list', 7 | method: 'get', 8 | params 9 | }) 10 | } 11 | 12 | // 批量删除操作日志 13 | export function batchDeleteOperationLogByIds(data) { 14 | return request({ 15 | url: '/api/log/operation/delete/batch', 16 | method: 'delete', 17 | data 18 | }) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/icons/svg/back-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/unit/utils/param2Obj.spec.js: -------------------------------------------------------------------------------- 1 | import { param2Obj } from '@/utils/index.js' 2 | describe('Utils:param2Obj', () => { 3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' 4 | 5 | it('param2Obj test', () => { 6 | expect(param2Obj(url)).toEqual({ 7 | name: 'bill', 8 | age: '29', 9 | sex: '1', 10 | field: window.btoa('test'), 11 | key: '测试' 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/api/system/base.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/api/base/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function refreshToken() { 12 | return request({ 13 | url: '/api/base/refreshToken', 14 | method: 'post' 15 | }) 16 | } 17 | 18 | export function logout() { 19 | return request({ 20 | url: '/api/base/logout', 21 | method: 'post' 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/close-circle-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/arrow-circle-up-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Tinymce/toolbar.js: -------------------------------------------------------------------------------- 1 | // Here is a list of the toolbar 2 | // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols 3 | 4 | const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'] 5 | 6 | export default toolbar 7 | -------------------------------------------------------------------------------- /src/icons/svg/data-transfer-both-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/vendor/publickey.js: -------------------------------------------------------------------------------- 1 | export const publicKey = `-----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsfk9FP4ZeVLYSzRMJpv4 3 | MSzm1WzM+3c9zfi98ldcLa7aQADzDi7vnk0TqXNMPY2r5Dmykrvf6HtC8DCEg01b 4 | oBapvkfJy0KdQ1XbGihOGrZKMqCvS6DUx8Gt1osc7ag13GcQC/pI+VNMrNO1wuZX 5 | veGEKsd6K8sw8Xc8DtyJMr2m/GVZftWSzzDu6/dHWFglFQ5nbLogiA7PDJc9m3Pu 6 | cMOWfHEHkeymvRTnTD0FlqjpWL7Dr42CdqtUqL4abQW6xJcbde5ow1SXzznO240m 7 | 5xDTnBxS5gh+uvz6NzENc2JUX1fP7W9zYwvV7/PMNoi47gQ61O5sp/H+vpmyXyV+ 8 | mwIDAQAB 9 | -----END PUBLIC KEY-----` 10 | -------------------------------------------------------------------------------- /src/api/dashboard/status.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getSystemInfo(params) { 4 | return request({ 5 | url: '/api/system/info', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | export function getStatus(params) { 11 | return request({ 12 | url: '/api/system/status', 13 | method: 'get', 14 | params 15 | }) 16 | } 17 | 18 | export function install(data) { 19 | return request({ 20 | url: '/api/system/install', 21 | method: 'post', 22 | data 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/api/console/routes.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getRoute(params) { 4 | return request({ 5 | url: '/api/console/route', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function switchRoute(data) { 12 | return request({ 13 | url: '/api/console/route', 14 | method: 'patch', 15 | data 16 | }) 17 | } 18 | 19 | export function deleteRoute(data) { 20 | return request({ 21 | url: '/api/console/route', 22 | method: 'delete', 23 | data 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/icons/svg/check-circle-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/api/console/preauthkey.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getPreAuthKeys() { 4 | return request({ 5 | url: '/api/console/preauthkey', 6 | method: 'get' 7 | }) 8 | } 9 | 10 | export function createPreAuthKey(data) { 11 | return request({ 12 | url: '/api/console/preauthkey', 13 | method: 'post', 14 | data 15 | }) 16 | } 17 | 18 | export function expirePreAuthKey(data) { 19 | return request({ 20 | url: '/api/console/preauthkey', 21 | method: 'delete', 22 | data 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/icons/svg/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/store/modules/errorLog.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | logs: [] 3 | } 4 | 5 | const mutations = { 6 | ADD_ERROR_LOG: (state, log) => { 7 | state.logs.push(log) 8 | }, 9 | CLEAR_ERROR_LOG: (state) => { 10 | state.logs.splice(0) 11 | } 12 | } 13 | 14 | const actions = { 15 | addErrorLog({ commit }, log) { 16 | commit('ADD_ERROR_LOG', log) 17 | }, 18 | clearErrorLog({ commit }) { 19 | commit('CLEAR_ERROR_LOG') 20 | } 21 | } 22 | 23 | export default { 24 | namespaced: true, 25 | state, 26 | mutations, 27 | actions 28 | } 29 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= webpackConfig.name %> 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/Tinymce/plugins.js: -------------------------------------------------------------------------------- 1 | // Any plugins you want to use has to be imported 2 | // Detail plugins list see https://www.tinymce.com/docs/plugins/ 3 | // Custom builds see https://www.tinymce.com/download/custom-builds/ 4 | 5 | const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount'] 6 | 7 | export default plugins 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app 4 | '@vue/cli-plugin-babel/preset' 5 | ], 6 | 'env': { 7 | 'development': { 8 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). 9 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. 10 | // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html 11 | 'plugins': ['dynamic-import-node'] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/ImageCropper/utils/data2blob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * database64文件格式转换为2进制 3 | * 4 | * @param {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了 5 | * @param {[String]} mime [description] 6 | * @return {[blob]} [description] 7 | */ 8 | export default function(data, mime) { 9 | data = data.split(',')[1] 10 | data = window.atob(data) 11 | var ia = new Uint8Array(data.length) 12 | for (var i = 0; i < data.length; i++) { 13 | ia[i] = data.charCodeAt(i) 14 | } 15 | // canvas.toDataURL 返回的默认格式就是 image/png 16 | return new Blob([ia], { 17 | type: mime 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/icons/svg/eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | /** 4 | * @param {Array} value 5 | * @returns {Boolean} 6 | * @example see @/views/permission/directive.vue 7 | */ 8 | export default function checkPermission(value) { 9 | if (value && value instanceof Array && value.length > 0) { 10 | const roles = store.getters && store.getters.roles 11 | const permissionRoles = value 12 | 13 | const hasPermission = roles.some(role => { 14 | return permissionRoles.includes(role) 15 | }) 16 | return hasPermission 17 | } else { 18 | console.error(`need roles! Like v-permission="['admin','editor']"`) 19 | return false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | size: state => state.app.size, 4 | device: state => state.app.device, 5 | visitedViews: state => state.tagsView.visitedViews, 6 | cachedViews: state => state.tagsView.cachedViews, 7 | token: state => state.user.token, 8 | avatar: state => state.user.avatar, 9 | name: state => state.user.name, 10 | introduction: state => state.user.introduction, 11 | roles: state => state.user.roles, 12 | permission_routes: state => state.permission.routes, 13 | errorLogs: state => state.errorLog.logs, 14 | routes: state => state.permission.routes 15 | } 16 | export default getters 17 | -------------------------------------------------------------------------------- /src/icons/svg/check-mark-button-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/vendor/Export2Zip.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { saveAs } from 'file-saver' 3 | import JSZip from 'jszip' 4 | 5 | export function export_txt_to_zip(th, jsonData, txtName, zipName) { 6 | const zip = new JSZip() 7 | const txt_name = txtName || 'file' 8 | const zip_name = zipName || 'file' 9 | const data = jsonData 10 | let txtData = `${th}\r\n` 11 | data.forEach((row) => { 12 | let tempStr = '' 13 | tempStr = row.toString() 14 | txtData += `${tempStr}\r\n` 15 | }) 16 | zip.file(`${txt_name}.txt`, txtData) 17 | zip.generateAsync({ 18 | type: "blob" 19 | }).then((blob) => { 20 | saveAs(blob, `${zip_name}.zip`) 21 | }, (err) => { 22 | alert('导出失败') 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/MarkdownEditor/default-options.js: -------------------------------------------------------------------------------- 1 | // doc: https://nhnent.github.io/tui.editor/api/latest/ToastUIEditor.html#ToastUIEditor 2 | export default { 3 | minHeight: '200px', 4 | previewStyle: 'vertical', 5 | useCommandShortcut: true, 6 | useDefaultHTMLSanitizer: true, 7 | usageStatistics: false, 8 | hideModeSwitch: false, 9 | toolbarItems: [ 10 | 'heading', 11 | 'bold', 12 | 'italic', 13 | 'strike', 14 | 'divider', 15 | 'hr', 16 | 'quote', 17 | 'divider', 18 | 'ul', 19 | 'ol', 20 | 'task', 21 | 'indent', 22 | 'outdent', 23 | 'divider', 24 | 'table', 25 | 'image', 26 | 'link', 27 | 'divider', 28 | 'code', 29 | 'codeblock' 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Clipboard from 'clipboard' 3 | 4 | function clipboardSuccess() { 5 | Vue.prototype.$message({ 6 | message: 'Copy successfully', 7 | type: 'success', 8 | duration: 1500 9 | }) 10 | } 11 | 12 | function clipboardError() { 13 | Vue.prototype.$message({ 14 | message: 'Copy failed', 15 | type: 'error' 16 | }) 17 | } 18 | 19 | export default function handleClipboard(text, event) { 20 | const clipboard = new Clipboard(event.target, { 21 | text: () => text 22 | }) 23 | clipboard.on('success', () => { 24 | clipboardSuccess() 25 | clipboard.destroy() 26 | }) 27 | clipboard.on('error', () => { 28 | clipboardError() 29 | clipboard.destroy() 30 | }) 31 | clipboard.onClick(event) 32 | } 33 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | 5 | Vue.use(Vuex) 6 | 7 | // https://webpack.js.org/guides/dependency-management/#requirecontext 8 | const modulesFiles = require.context('./modules', true, /\.js$/) 9 | 10 | // you do not need `import app from './modules/app'` 11 | // it will auto require all vuex module from modules file 12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 13 | // set './app.js' => 'app' 14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 15 | const value = modulesFiles(modulePath) 16 | modules[moduleName] = value.default 17 | return modules 18 | }, {}) 19 | 20 | const store = new Vuex.Store({ 21 | modules, 22 | getters 23 | }) 24 | 25 | export default store 26 | -------------------------------------------------------------------------------- /src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/directive/permission/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | function checkPermission(el, binding) { 4 | const { value } = binding 5 | const roles = store.getters && store.getters.roles 6 | 7 | if (value && value instanceof Array) { 8 | if (value.length > 0) { 9 | const permissionRoles = value 10 | 11 | const hasPermission = roles.some(role => { 12 | return permissionRoles.includes(role) 13 | }) 14 | 15 | if (!hasPermission) { 16 | el.parentNode && el.parentNode.removeChild(el) 17 | } 18 | } 19 | } else { 20 | throw new Error(`need roles! Like v-permission="['admin','editor']"`) 21 | } 22 | } 23 | 24 | export default { 25 | inserted(el, binding) { 26 | checkPermission(el, binding) 27 | }, 28 | update(el, binding) { 29 | checkPermission(el, binding) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import variables from '@/styles/element-variables.scss' 2 | import defaultSettings from '@/settings' 3 | 4 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings 5 | 6 | const state = { 7 | theme: variables.theme, 8 | showSettings: showSettings, 9 | tagsView: tagsView, 10 | fixedHeader: fixedHeader, 11 | sidebarLogo: sidebarLogo 12 | } 13 | 14 | const mutations = { 15 | CHANGE_SETTING: (state, { key, value }) => { 16 | // eslint-disable-next-line no-prototype-builtins 17 | if (state.hasOwnProperty(key)) { 18 | state[key] = value 19 | } 20 | } 21 | } 22 | 23 | const actions = { 24 | changeSetting({ commit }, data) { 25 | commit('CHANGE_SETTING', data) 26 | } 27 | } 28 | 29 | export default { 30 | namespaced: true, 31 | state, 32 | mutations, 33 | actions 34 | } 35 | 36 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /src/api/console/machines.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getMachines(params) { 4 | return request({ 5 | url: '/api/console/machine', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function postMachine(data) { 12 | return request({ 13 | url: '/api/console/machine', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | 19 | export function deleteMachine(data) { 20 | return request({ 21 | url: '/api/console/machine', 22 | method: 'delete', 23 | data 24 | }) 25 | } 26 | 27 | export function updateTags(data) { 28 | return request({ 29 | url: '/api/console/machine', 30 | method: 'patch', 31 | data 32 | }) 33 | } 34 | 35 | export function moveMachine(data) { 36 | return request({ 37 | url: '/api/console/machine', 38 | method: 'put', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/icons/svg/zip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // base color 2 | $blue:#324157; 3 | $light-blue:#3A71A8; 4 | $red:#C03639; 5 | $pink: #E65D6E; 6 | $green: #30B08F; 7 | $tiffany: #4AB7BD; 8 | $yellow:#FEC171; 9 | $panGreen: #30B08F; 10 | 11 | // sidebar 12 | $menuText:#bfcbd9; 13 | $menuActiveText:#409EFF; 14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 15 | 16 | $menuBg:#304156; 17 | $menuHover:#263445; 18 | 19 | $subMenuBg:#1f2d3d; 20 | $subMenuHover:#001528; 21 | 22 | $sideBarWidth: 210px; 23 | 24 | // the :export directive is the magic sauce for webpack 25 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 26 | :export { 27 | menuText: $menuText; 28 | menuActiveText: $menuActiveText; 29 | subMenuActiveText: $subMenuActiveText; 30 | menuBg: $menuBg; 31 | menuHover: $menuHover; 32 | subMenuBg: $subMenuBg; 33 | subMenuHover: $subMenuHover; 34 | sideBarWidth: $sideBarWidth; 35 | } 36 | -------------------------------------------------------------------------------- /src/api/system/api.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取接口列表 4 | export function getApis(params) { 5 | return request({ 6 | url: '/api/api/list', 7 | method: 'get', 8 | params 9 | }) 10 | } 11 | 12 | // 获取接口树(按接口Category字段分类) 13 | export function getApiTree(params) { 14 | return request({ 15 | url: '/api/api/tree', 16 | method: 'get', 17 | params 18 | }) 19 | } 20 | 21 | // 创建接口 22 | export function createApi(data) { 23 | return request({ 24 | url: '/api/api/create', 25 | method: 'post', 26 | data 27 | }) 28 | } 29 | 30 | // 更新接口 31 | export function updateApiById(Id, data) { 32 | return request({ 33 | url: '/api/api/update/' + Id, 34 | method: 'patch', 35 | data 36 | }) 37 | } 38 | 39 | // 批量删除接口 40 | export function batchDeleteApiByIds(data) { 41 | return request({ 42 | url: '/api/api/delete/batch', 43 | method: 'delete', 44 | data 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /src/directive/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /src/styles/element-variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * I think element-ui's default theme color is too light for long-term use. 3 | * So I modified the default color and you can modify it to your liking. 4 | **/ 5 | 6 | /* theme color */ 7 | $--color-primary: #1890ff; 8 | $--color-success: #13ce66; 9 | $--color-warning: #ffba00; 10 | $--color-danger: #ff4949; 11 | // $--color-info: #1E1E1E; 12 | 13 | $--button-font-weight: 400; 14 | 15 | // $--color-text-regular: #1f2d3d; 16 | 17 | $--border-color-light: #dfe4ed; 18 | $--border-color-lighter: #e6ebf5; 19 | 20 | $--table-border: 1px solid #dfe6ec; 21 | 22 | /* icon font path, required */ 23 | $--font-path: "~element-ui/lib/theme-chalk/fonts"; 24 | 25 | @import "~element-ui/packages/theme-chalk/src/index"; 26 | 27 | // the :export directive is the magic sauce for webpack 28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 29 | :export { 30 | theme: $--color-primary; 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /docs/nginx.md: -------------------------------------------------------------------------------- 1 | 如果你想要将前端与后端部署到同一个服务器上,建议你使用nginx或其他能够同时提供 2 | 3 | ``` 4 | server 5 | { 6 | listen 80; 7 | listen [::]:80; 8 | server_name example.com; 9 | 10 | # headscale-panel 11 | location ~ ^/(api|\.well-known/openid-configuration) { 12 | proxy_pass http://localhost:8088; 13 | proxy_set_header Host $host; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | } 17 | 18 | # headscale 19 | location ~ ^/(health|oidc|windows|apple|key|register|drep|bootstrap-dns|swagger|ts2021) { 20 | proxy_pass http://localhost:8080; 21 | proxy_set_header Host $host; 22 | proxy_set_header X-Real-IP $remote_addr; 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | } 25 | 26 | location / { 27 | root /path/to/your/static/files; 28 | index index.html; 29 | } 30 | } 31 | ``` -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Headscale Panel', 3 | 4 | /** 5 | * @type {boolean} true | false 6 | * @description Whether show the settings right-panel 7 | */ 8 | showSettings: false, 9 | 10 | /** 11 | * @type {boolean} true | false 12 | * @description Whether need tagsView 13 | */ 14 | tagsView: false, 15 | 16 | /** 17 | * @type {boolean} true | false 18 | * @description Whether fix the header 19 | */ 20 | fixedHeader: false, 21 | 22 | /** 23 | * @type {boolean} true | false 24 | * @description Whether show the logo in sidebar 25 | */ 26 | sidebarLogo: false, 27 | 28 | /** 29 | * @type {string | array} 'production' | ['production', 'development'] 30 | * @description Need show err logs component. 31 | * The default is only used in the production env 32 | * If you want to also use it in dev, you can pass ['production', 'development'] 33 | */ 34 | errorLog: 'production' 35 | } 36 | -------------------------------------------------------------------------------- /src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/svg/eye-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/pdf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/utils/error-log.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | import { isString, isArray } from '@/utils/validate' 4 | import settings from '@/settings' 5 | 6 | // you can set in settings.js 7 | // errorLog:'production' | ['production', 'development'] 8 | const { errorLog: needErrorLog } = settings 9 | 10 | function checkNeed() { 11 | const env = process.env.NODE_ENV 12 | if (isString(needErrorLog)) { 13 | return env === needErrorLog 14 | } 15 | if (isArray(needErrorLog)) { 16 | return needErrorLog.includes(env) 17 | } 18 | return false 19 | } 20 | 21 | if (checkNeed()) { 22 | Vue.config.errorHandler = function(err, vm, info, a) { 23 | // Don't ask me why I use Vue.nextTick, it just a hack. 24 | // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500 25 | Vue.nextTick(() => { 26 | store.dispatch('errorLog/addErrorLog', { 27 | err, 28 | vm, 29 | info, 30 | url: window.location.href 31 | }) 32 | console.error(err, info) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/api/system/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取当前登录用户信息 4 | export function getInfo() { 5 | return request({ 6 | url: '/api/user/info', 7 | method: 'post' 8 | }) 9 | } 10 | 11 | // 获取用户列表 12 | export function getUsers(params) { 13 | return request({ 14 | url: '/api/user/list', 15 | method: 'get', 16 | params 17 | }) 18 | } 19 | 20 | // 更新用户登录密码 21 | export function changePwd(data) { 22 | return request({ 23 | url: '/api/user/changePwd', 24 | method: 'put', 25 | data 26 | }) 27 | } 28 | 29 | // 创建用户 30 | export function createUser(data) { 31 | return request({ 32 | url: '/api/user/create', 33 | method: 'post', 34 | data 35 | }) 36 | } 37 | 38 | // 更新用户 39 | export function updateUserById(id, data) { 40 | return request({ 41 | url: '/api/user/update/' + id, 42 | method: 'patch', 43 | data 44 | }) 45 | } 46 | 47 | // 批量删除用户 48 | export function batchDeleteUserByIds(data) { 49 | return request({ 50 | url: '/api/user/delete/batch', 51 | method: 'delete', 52 | data 53 | }) 54 | } 55 | 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 QianheYu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import Cookies from 'js-cookie' 4 | 5 | import 'normalize.css/normalize.css' // a modern alternative to CSS resets 6 | 7 | import Element from 'element-ui' 8 | import './styles/element-variables.scss' 9 | 10 | import '@/styles/index.scss' // global css 11 | 12 | import App from './App' 13 | import store from './store' 14 | import router from './router' 15 | 16 | import './icons' // icon 17 | import './permission' // permission control 18 | // import './utils/error-log' // error log 19 | import i18n from './lang' 20 | 21 | import * as filters from './filters' // global filters 22 | 23 | import VueDeviceDetector from 'vue-device-detector' 24 | Vue.use(VueDeviceDetector) 25 | 26 | Vue.use(Element, { 27 | size: Cookies.get('size') || 'medium', // set element-ui default size 28 | i18n: (key, value) => i18n.t(key, value) 29 | }) 30 | 31 | // register global utility filters 32 | Object.keys(filters).forEach(key => { 33 | Vue.filter(key, filters[key]) 34 | }) 35 | 36 | Vue.config.productionTip = false 37 | 38 | new Vue({ 39 | el: '#app', 40 | router, 41 | i18n, 42 | store, 43 | render: h => h(App) 44 | }) 45 | -------------------------------------------------------------------------------- /tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | describe('Utils:formatTime', () => { 3 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 4 | const retrofit = 5 * 1000 5 | 6 | it('ten digits timestamp', () => { 7 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 8 | }) 9 | it('test now', () => { 10 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 11 | }) 12 | it('less two minute', () => { 13 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 14 | }) 15 | it('less two hour', () => { 16 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 17 | }) 18 | it('less one day', () => { 19 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 20 | }) 21 | it('more than one day', () => { 22 | expect(formatTime(d)).toBe('7月13日17时54分') 23 | }) 24 | it('format', () => { 25 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 26 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 27 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/views/profile/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 55 | -------------------------------------------------------------------------------- /src/icons/svg/upload-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/download-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/headscale3-dots.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/dashboard/components/SystemInfoGroup.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 36 | -------------------------------------------------------------------------------- /src/icons/svg/power-up-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 49 | 50 | 58 | -------------------------------------------------------------------------------- /src/icons/svg/power-down-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/api/system/menu.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取菜单树 4 | export function getMenuTree() { 5 | return request({ 6 | url: '/api/menu/tree', 7 | method: 'get' 8 | }) 9 | } 10 | 11 | // 获取菜单列表 12 | export function getMenus() { 13 | return request({ 14 | url: '/api/menu/list', 15 | method: 'get' 16 | }) 17 | } 18 | 19 | // 创建菜单 20 | export function createMenu(data) { 21 | return request({ 22 | url: '/api/menu/create', 23 | method: 'post', 24 | data 25 | }) 26 | } 27 | 28 | // 更新菜单 29 | export function updateMenuById(Id, data) { 30 | return request({ 31 | url: '/api/menu/update/' + Id, 32 | method: 'patch', 33 | data 34 | }) 35 | } 36 | 37 | // 批量删除菜单 38 | export function batchDeleteMenuByIds(data) { 39 | return request({ 40 | url: '/api/menu/delete/batch', 41 | method: 'delete', 42 | data 43 | }) 44 | } 45 | 46 | // 获取用户的可访问菜单列表 47 | export function getUserMenusByUserId(Id) { 48 | return request({ 49 | url: '/api/menu/access/list/' + Id, 50 | method: 'get' 51 | }) 52 | } 53 | 54 | // 获取用户的可访问菜单树 55 | export function getUserMenuTreeByUserId(Id) { 56 | return request({ 57 | url: '/api/menu/access/tree/' + Id, 58 | method: 'get' 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, validURL, validLowerCase, validUpperCase, validAlphabets } from '@/utils/validate.js' 2 | describe('Utils:validate', () => { 3 | it('validUsername', () => { 4 | expect(validUsername('admin')).toBe(true) 5 | expect(validUsername('editor')).toBe(true) 6 | expect(validUsername('xxxx')).toBe(false) 7 | }) 8 | it('validURL', () => { 9 | expect(validURL('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 10 | expect(validURL('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(validURL('github.com/PanJiaChen/vue-element-admin')).toBe(false) 12 | }) 13 | it('validLowerCase', () => { 14 | expect(validLowerCase('abc')).toBe(true) 15 | expect(validLowerCase('Abc')).toBe(false) 16 | expect(validLowerCase('123abc')).toBe(false) 17 | }) 18 | it('validUpperCase', () => { 19 | expect(validUpperCase('ABC')).toBe(true) 20 | expect(validUpperCase('Abc')).toBe(false) 21 | expect(validUpperCase('123ABC')).toBe(false) 22 | }) 23 | it('validAlphabets', () => { 24 | expect(validAlphabets('ABC')).toBe(true) 25 | expect(validAlphabets('Abc')).toBe(true) 26 | expect(validAlphabets('123aBC')).toBe(false) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/open-window.js: -------------------------------------------------------------------------------- 1 | /** 2 | *Created by PanJiaChen on 16/11/29. 3 | * @param {Sting} url 4 | * @param {Sting} title 5 | * @param {Number} w 6 | * @param {Number} h 7 | */ 8 | export default function openWindow(url, title, w, h) { 9 | // Fixes dual-screen position Most browsers Firefox 10 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left 11 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top 12 | 13 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width 14 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height 15 | 16 | const left = ((width / 2) - (w / 2)) + dualScreenLeft 17 | const top = ((height / 2) - (h / 2)) + dualScreenTop 18 | const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left) 19 | 20 | // Puts focus on the newWindow 21 | if (window.focus) { 22 | newWindow.focus() 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/components/LangSelect/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 42 | -------------------------------------------------------------------------------- /tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | 9 | it('timestamp string', () => { 10 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 11 | }) 12 | 13 | it('ten digits timestamp', () => { 14 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 15 | }) 16 | it('new Date', () => { 17 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 18 | }) 19 | it('format', () => { 20 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 21 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 22 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 23 | }) 24 | it('get the day of the week', () => { 25 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 26 | }) 27 | it('get the day of the week', () => { 28 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 29 | }) 30 | it('empty argument', () => { 31 | expect(parseTime()).toBeNull() 32 | }) 33 | 34 | it('null', () => { 35 | expect(parseTime(null)).toBeNull() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/directive/el-table/adaptive.js: -------------------------------------------------------------------------------- 1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event' 2 | 3 | /** 4 | * How to use 5 | * ... 6 | * el-table height is must be set 7 | * bottomOffset: 30(default) // The height of the table from the bottom of the page. 8 | */ 9 | 10 | const doResize = (el, binding, vnode) => { 11 | const { componentInstance: $table } = vnode 12 | 13 | const { value } = binding 14 | 15 | if (!$table.height) { 16 | throw new Error(`el-$table must set the height. Such as height='100px'`) 17 | } 18 | const bottomOffset = (value && value.bottomOffset) || 30 19 | 20 | if (!$table) return 21 | 22 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset 23 | $table.layout.setHeight(height) 24 | $table.doLayout() 25 | } 26 | 27 | export default { 28 | bind(el, binding, vnode) { 29 | el.resizeListener = () => { 30 | doResize(el, binding, vnode) 31 | } 32 | // parameter 1 is must be "Element" type 33 | addResizeListener(window.document.body, el.resizeListener) 34 | }, 35 | inserted(el, binding, vnode) { 36 | doResize(el, binding, vnode) 37 | }, 38 | unbind(el) { 39 | removeResizeListener(window.document.body, el.resizeListener) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /plop-templates/view/prompt.js: -------------------------------------------------------------------------------- 1 | const { notEmpty } = require('../utils.js') 2 | 3 | module.exports = { 4 | description: 'generate a view', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: 'view name please', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'checkbox', 13 | name: 'blocks', 14 | message: 'Blocks:', 15 | choices: [{ 16 | name: '