├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── build └── index.js ├── jest.config.js ├── jsconfig.json ├── package.json ├── plopfile.js ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── server ├── Dockerfile ├── apis │ ├── chat.go │ ├── init.go │ ├── menu.go │ ├── path.go │ ├── role.go │ └── user.go ├── configs │ ├── application.go │ ├── config.go │ ├── config.yaml │ └── database.go ├── db │ └── mysql.go ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml ├── go-element-admin.sql ├── go.mod ├── go.sum ├── main.go ├── middlewares │ ├── auth.go │ └── role.go ├── models │ ├── menu.go │ ├── path.go │ ├── role.go │ ├── user.go │ ├── user_log.go │ └── user_role.go ├── routers │ └── router.go └── utils │ ├── gcasbin │ └── gcasbin.go │ ├── jwt.go │ ├── log.go │ └── tools.go ├── src ├── App.vue ├── api │ ├── system │ │ ├── menu.js │ │ ├── path.js │ │ ├── role.js │ │ ├── user-log.js │ │ └── user.js │ └── user.js ├── assets │ ├── 403_images │ │ └── 403.gif │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ ├── custom-theme │ │ ├── fonts │ │ │ ├── element-icons.ttf │ │ │ └── element-icons.woff │ │ └── index.css │ ├── pay_wechat.png │ └── pay_zfb.png ├── components │ ├── BackToTop │ │ └── index.vue │ ├── Breadcrumb │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── HeaderSearch │ │ └── index.vue │ ├── IconSelect │ │ ├── index.vue │ │ └── requireIcons.js │ ├── Pagination │ │ └── index.vue │ ├── RightPanel │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── SizeSelect │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── TextHoverEffect │ │ └── Mallki.vue │ └── ThemePicker │ │ └── index.vue ├── directive │ └── permission │ │ ├── index.js │ │ └── permission.js ├── filters │ └── index.js ├── icons │ ├── index.js │ ├── svg │ │ ├── 404.svg │ │ ├── bug.svg │ │ ├── build.svg │ │ ├── calendar.svg │ │ ├── cascader.svg │ │ ├── chart.svg │ │ ├── checkbox.svg │ │ ├── clipboard.svg │ │ ├── code.svg │ │ ├── color.svg │ │ ├── component.svg │ │ ├── dashboard.svg │ │ ├── date-range.svg │ │ ├── date.svg │ │ ├── dict.svg │ │ ├── documentation.svg │ │ ├── download.svg │ │ ├── drag copy.svg │ │ ├── drag.svg │ │ ├── druid.svg │ │ ├── edit.svg │ │ ├── education.svg │ │ ├── email.svg │ │ ├── example.svg │ │ ├── excel.svg │ │ ├── exit-fullscreen.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── form.svg │ │ ├── fullscreen.svg │ │ ├── github.svg │ │ ├── guide.svg │ │ ├── icon.svg │ │ ├── input.svg │ │ ├── international.svg │ │ ├── job.svg │ │ ├── language.svg │ │ ├── link.svg │ │ ├── list.svg │ │ ├── lock.svg │ │ ├── log.svg │ │ ├── logininfor.svg │ │ ├── message.svg │ │ ├── money.svg │ │ ├── monitor.svg │ │ ├── nested.svg │ │ ├── network.svg │ │ ├── number.svg │ │ ├── online.svg │ │ ├── pass.svg │ │ ├── password.svg │ │ ├── pdf.svg │ │ ├── people.svg │ │ ├── peoples.svg │ │ ├── phone.svg │ │ ├── post.svg │ │ ├── qq.svg │ │ ├── question.svg │ │ ├── radio.svg │ │ ├── rate.svg │ │ ├── row.svg │ │ ├── search.svg │ │ ├── select.svg │ │ ├── server.svg │ │ ├── shopping.svg │ │ ├── size.svg │ │ ├── skill.svg │ │ ├── slider.svg │ │ ├── star.svg │ │ ├── swagger.svg │ │ ├── switch.svg │ │ ├── system.svg │ │ ├── tab.svg │ │ ├── table.svg │ │ ├── textarea.svg │ │ ├── theme.svg │ │ ├── time-range.svg │ │ ├── time.svg │ │ ├── tool.svg │ │ ├── tree-table.svg │ │ ├── tree.svg │ │ ├── upload.svg │ │ ├── user.svg │ │ ├── validCode.svg │ │ ├── vip.svg │ │ ├── wechat.svg │ │ └── zip.svg │ └── svgo.yml ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ ├── Settings │ │ │ └── index.vue │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── Logo.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ ├── TagsView │ │ │ ├── ScrollPane.vue │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── permission.js ├── router │ └── index.js ├── settings.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── chat.js │ │ ├── permission.js │ │ ├── settings.js │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── btn.scss │ ├── element-ui.scss │ ├── element-variables.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── costum.js │ ├── get-page-title.js │ ├── index.js │ ├── open-window.js │ ├── permission.js │ ├── request.js │ ├── scroll-to.js │ ├── validate.js │ └── websocket.js └── views │ ├── dashboard │ ├── components │ │ ├── Chat.vue │ │ └── mixins │ │ │ └── resize.js │ └── index.vue │ ├── demo │ ├── demo-1 │ │ ├── demo-1-1 │ │ │ └── index.vue │ │ └── index.vue │ └── demo-2 │ │ ├── demo-2-1 │ │ └── index.vue │ │ └── index.vue │ ├── error-page │ ├── 403.vue │ └── 404.vue │ ├── login │ ├── auth-redirect.vue │ └── index.vue │ ├── redirect │ └── index.vue │ └── system │ ├── log │ └── index.vue │ ├── menu │ └── index.vue │ ├── path │ └── index.vue │ ├── role │ └── index.vue │ └── user │ └── index.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── Hamburger.spec.js │ └── SvgIcon.spec.js │ └── utils │ ├── formatTime.spec.js │ ├── parseTime.spec.js │ └── validate.spec.js └── vue.config.js /.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 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api' 6 | 7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 9 | # It only does one thing by converting all import() to require(). 10 | # This configuration can significantly increase the speed of hot updates, 11 | # when you have a large number of pages. 12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/app.js 13 | 14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 15 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api' 6 | 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 bigfool-cn 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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 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 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigfool-cn/go-element-admin/3297eb1cc7f0f1c3149e16d5d59d76fb6e37b4ae/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= webpackConfig.name %> 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | # 开启Go Module, 设置GO Proxy代理 4 | ENV GO111MODULE=on \ 5 | GOPROXY=https://goproxy.cn,direct 6 | 7 | # 新建项目目录 8 | RUN mkdir -p /go-element-admin 9 | 10 | # 指定工作目录 11 | WORKDIR /go-element-admin 12 | 13 | # 复制源代码到工作目录 14 | COPY . . 15 | 16 | # 删除旧的可执行文件 17 | RUN rm -rf /go-element-admin/server 18 | 19 | # 设置操作系统, 操作系统构架 20 | RUN GOOS=linux GOARCH=amd64 21 | 22 | RUN go build -o server . 23 | 24 | # 添加可执行权限 25 | RUN chmod +x /go-element-admin/server 26 | 27 | 28 | FROM alpine 29 | 30 | # MAINTAINER 31 | LABEL name="go-element-admin" 32 | LABEL version="1.0.1" 33 | LABEL author="bigfool <1063944784@qq.com>" 34 | LABEL maintainer="bigfool <1063944784@qq.com>" 35 | LABEL description="go-element-admin application" 36 | 37 | # 复制builder相关文件到基础镜像alpine 38 | COPY --from=builder /go-element-admin/configs/config.yaml /go-element-admin/configs/config.yaml 39 | COPY --from=builder /go-element-admin/server /go-element-admin 40 | 41 | # 设置时区 42 | RUN apk add -U tzdata \ 43 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/lcoaltime \ 44 | && echo 'Asia/Shanghai' > /etc/timezone 45 | 46 | ENV TZ=Asia/Shanghai 47 | 48 | # 创建日志目录 49 | RUN mkdir -p /go-element-admin/logs 50 | 51 | # 新建一个用户www 并设置项目目录用户组 52 | RUN adduser -D -H www \ 53 | && chown -R www /go-element-admin 54 | 55 | # 执行用户 56 | USER www 57 | 58 | WORKDIR /go-element-admin 59 | 60 | EXPOSE 8001 61 | 62 | ENTRYPOINT ["./server"] 63 | -------------------------------------------------------------------------------- /server/apis/init.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "go-element-admin/models" 5 | "go-element-admin/utils" 6 | ) 7 | 8 | var lgr = utils.DefaultLogger(false) 9 | 10 | type Res struct { 11 | Code int `json:"code"` 12 | Data interface{} `json:"data"` 13 | Message string `json:"message"` 14 | } 15 | 16 | // 登录表单 17 | type loginForm struct { 18 | UserName string `json:"user_name" binding:"required"` 19 | Password string `json:"password" binding:"required"` 20 | } 21 | 22 | // 登录账号信息 23 | type info struct { 24 | Avatar string `json:"avatar"` 25 | Buttons []string `json:"buttons"` 26 | Menus []models.TreeMenu `json:"menus"` 27 | Roles []string `json:"roles"` 28 | UserID int64 `json:"user_id"` 29 | } 30 | 31 | // 角色表单 32 | type roleForm struct { 33 | models.RoleView 34 | } 35 | 36 | // 账号信息表单--添加 37 | type userCreateForm struct { 38 | userUpdateForm 39 | passwordToRe 40 | } 41 | 42 | // 账号信息表单--修改 43 | type userUpdateForm struct { 44 | UserName string `json:"user_name" binding:"required"` 45 | Status int `json:"status" binding:"numeric"` 46 | RoleIds []int64 `json:"role_ids" binding:"required"` 47 | } 48 | 49 | // 账号密码表单--修改 50 | type userUpdatePwdForm struct { 51 | OldPassword string `json:"old_password" binding:"required"` 52 | passwordToRe 53 | } 54 | 55 | type passwordToRe struct { 56 | Password string `json:"password" binding:"required"` 57 | Repassword string `json:"repassword" binding:"required,eqfield=Password"` 58 | } 59 | 60 | type menuForm struct { 61 | models.Menu 62 | } 63 | 64 | type pathForm struct { 65 | models.Path 66 | } 67 | -------------------------------------------------------------------------------- /server/configs/application.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import "github.com/spf13/viper" 4 | 5 | type Application struct { 6 | Host string 7 | Port string 8 | Name string 9 | JwtSecret string 10 | LogPath string 11 | Debug bool 12 | } 13 | 14 | func InitApplication(cfg *viper.Viper) *Application { 15 | return &Application{ 16 | Host: cfg.GetString("host"), 17 | Port: cfg.GetString("port"), 18 | Name: cfg.GetString("name"), 19 | JwtSecret: cfg.GetString("jwtSecret"), 20 | LogPath: cfg.GetString("logPath"), 21 | Debug: cfg.GetBool("debug"), 22 | } 23 | } 24 | 25 | var ApplicationConfig = new(Application) 26 | -------------------------------------------------------------------------------- /server/configs/config.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | ) 7 | 8 | var cfgDatabase *viper.Viper 9 | var cfgApplication *viper.Viper 10 | 11 | func init() { 12 | viper.SetConfigName("config") // name of config file (without extension) 13 | viper.AddConfigPath("./configs") // optionally look for config in the working directory 14 | err := viper.ReadInConfig() // Find and read the config file 15 | if err != nil { 16 | log.Println(err) // Handle errors reading the config file 17 | } 18 | 19 | cfgDatabase = viper.Sub("database") 20 | if cfgDatabase == nil { 21 | panic("config not found database") 22 | } 23 | DatabaseConfig = InitDatabase(cfgDatabase) 24 | 25 | cfgApplication = viper.Sub("application") 26 | if cfgApplication == nil { 27 | panic("config not found application") 28 | } 29 | ApplicationConfig = InitApplication(cfgApplication) 30 | } 31 | -------------------------------------------------------------------------------- /server/configs/config.yaml: -------------------------------------------------------------------------------- 1 | application: 2 | name: go-element-admin 3 | host: 0.0.0.0 4 | port: 8001 5 | jwtsecret: 7a8010aed1454231d488a0e19c40be4a 6 | logpath: /go-element-admin/logs 7 | debug: false 8 | database: 9 | database: go-element-admin 10 | dbtype: mysql 11 | host: 127.0.0.1 12 | port: 3306 13 | username: root 14 | password: 123456 15 | 16 | -------------------------------------------------------------------------------- /server/configs/database.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import "github.com/spf13/viper" 4 | 5 | type Database struct { 6 | Database string 7 | Dbtype string 8 | Host string 9 | Password string 10 | Port int 11 | Username string 12 | } 13 | 14 | func InitDatabase(cfg *viper.Viper) *Database { 15 | return &Database{ 16 | Database: cfg.GetString("database"), 17 | Dbtype: cfg.GetString("dbtype"), 18 | Host: cfg.GetString("host"), 19 | Port: cfg.GetInt("port"), 20 | Password: cfg.GetString("password"), 21 | Username: cfg.GetString("username"), 22 | } 23 | } 24 | 25 | var DatabaseConfig = new(Database) -------------------------------------------------------------------------------- /server/db/mysql.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "bytes" 5 | _ "github.com/go-sql-driver/mysql" //加载mysql 6 | "github.com/jinzhu/gorm" 7 | "go-element-admin/configs" 8 | "go-element-admin/utils" 9 | "strconv" 10 | ) 11 | 12 | var Eloquent *gorm.DB 13 | 14 | var lgr = utils.DefaultLogger(false) 15 | 16 | func init() { 17 | 18 | dbType := configs.DatabaseConfig.Dbtype 19 | host := configs.DatabaseConfig.Host 20 | port := configs.DatabaseConfig.Port 21 | database := configs.DatabaseConfig.Database 22 | username := configs.DatabaseConfig.Username 23 | password := configs.DatabaseConfig.Password 24 | 25 | if dbType != "mysql" { 26 | lgr.Error("db type unknow") 27 | } 28 | var err error 29 | 30 | var conn bytes.Buffer 31 | conn.WriteString(username) 32 | conn.WriteString(":") 33 | conn.WriteString(password) 34 | conn.WriteString("@tcp(") 35 | conn.WriteString(host) 36 | conn.WriteString(":") 37 | conn.WriteString(strconv.Itoa(port)) 38 | conn.WriteString(")") 39 | conn.WriteString("/") 40 | conn.WriteString(database) 41 | conn.WriteString("?charset=utf8&parseTime=True&loc=Local&timeout=1000ms") 42 | 43 | lgr.Println(conn.String()) 44 | 45 | var db Database 46 | if dbType == "mysql" { 47 | db = new(Mysql) 48 | } else { 49 | panic("db type unknow") 50 | } 51 | 52 | Eloquent, err = db.Open(dbType, conn.String()) 53 | 54 | Eloquent.LogMode(configs.ApplicationConfig.Debug) 55 | 56 | if err != nil { 57 | lgr.Errorf("mysql connect error %v", err) 58 | } else { 59 | lgr.Println("mysql connect success!") 60 | } 61 | 62 | if Eloquent.Error != nil { 63 | lgr.Errorf("database error %v", Eloquent.Error) 64 | } 65 | 66 | } 67 | 68 | type Database interface { 69 | Open(dbType string, conn string) (db *gorm.DB, err error) 70 | } 71 | 72 | type Mysql struct { 73 | } 74 | 75 | func (*Mysql) Open(dbType string, conn string) (db *gorm.DB, err error) { 76 | eloquent, err := gorm.Open(dbType, conn) 77 | return eloquent, err 78 | } 79 | 80 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module go-element-admin 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 7 | github.com/casbin/casbin/v2 v2.14.2 8 | github.com/casbin/gorm-adapter/v3 v3.0.3 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 10 | github.com/gin-gonic/gin v1.6.3 11 | github.com/go-openapi/spec v0.19.9 // indirect 12 | github.com/go-openapi/swag v0.19.9 // indirect 13 | github.com/go-sql-driver/mysql v1.5.0 14 | github.com/gorilla/websocket v1.4.2 15 | github.com/jinzhu/gorm v1.9.14 16 | github.com/mailru/easyjson v0.7.6 // indirect 17 | github.com/sirupsen/logrus v1.4.2 18 | github.com/spf13/viper v1.7.0 19 | github.com/swaggo/gin-swagger v1.2.0 20 | github.com/swaggo/swag v1.6.7 21 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect 22 | golang.org/x/tools v0.0.0-20200913032122-97363e29fc9b // indirect 23 | gopkg.in/yaml.v2 v2.3.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "log" 6 | "go-element-admin/configs" 7 | "go-element-admin/routers" 8 | ) 9 | 10 | // @title Go-Admin-Element 11 | // @description Go-Admin-Element后端API文档 12 | // @host 127.0.0.1:8001 13 | // @BasePath 14 | func main() { 15 | if configs.ApplicationConfig.Debug { 16 | gin.SetMode(gin.DebugMode) 17 | } else { 18 | gin.SetMode(gin.ReleaseMode) 19 | } 20 | r := routers.InitRouter() 21 | if err := r.Run(configs.ApplicationConfig.Host + ":" + configs.ApplicationConfig.Port); err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /server/middlewares/auth.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go-element-admin/apis" 6 | "go-element-admin/utils" 7 | "net/http" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | var lgr = utils.DefaultLogger(false) 13 | 14 | // 验证jwt令牌 15 | func JwtMiddleWare() gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | token := c.GetHeader("Authorization") 18 | if token == "" { 19 | c.JSON(http.StatusUnauthorized,apis.Res{Code:401,Message:"token不可用"}) 20 | c.Abort() 21 | return 22 | } else { 23 | claims, err := utils.Jwt.ParseToken(token) 24 | if err != nil { 25 | lgr.Errorf("解析token失败:%v",err.Error()) 26 | c.JSON(http.StatusUnauthorized,apis.Res{Code:401,Message:"token不可用"}) 27 | c.Abort() 28 | return 29 | } else if time.Now().Unix() > claims.ExpiresAt { 30 | c.JSON(http.StatusUnauthorized,apis.Res{Code:401,Message:"token已过期"}) 31 | c.Abort() 32 | return 33 | } 34 | c.Set("_user_id",claims.UserId) 35 | c.Set("_user_name",claims.UserName) 36 | c.Set("_user_roles",strings.Join(utils.SliceInt64ToString(claims.Roles),",")) 37 | } 38 | c.Next() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/middlewares/role.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "go-element-admin/utils/gcasbin" 5 | "github.com/gin-gonic/gin" 6 | "strings" 7 | ) 8 | 9 | // role casbin middleware 权限认证中间件 10 | func RoleCasbinMiddleWare() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | // 请求的path 13 | p := c.Request.URL.Path 14 | // 请求的方法 15 | m := c.Request.Method 16 | rolesStr, bol := c.Get("_user_roles") 17 | if !bol { 18 | c.JSON(403, gin.H{ 19 | "code": 403, 20 | "message": "没有操作权限", 21 | "data": "", 22 | }) 23 | c.Abort() 24 | return 25 | } 26 | 27 | roles := strings.Split(rolesStr.(string),",") 28 | 29 | var abort = true 30 | for _,role := range roles { 31 | res, _ := gcasbin.Enforcer.Enforce(role,p,m) 32 | if res == true { 33 | abort = false 34 | break 35 | } 36 | } 37 | if abort == true { 38 | c.JSON(403, gin.H{ 39 | "code": 403, 40 | "message": "没有操作权限", 41 | "data": "", 42 | }) 43 | c.Abort() 44 | return 45 | } 46 | c.Next() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/models/user_log.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | orm "go-element-admin/db" 6 | "go-element-admin/utils" 7 | ) 8 | 9 | type UserLog struct { 10 | UserLogID int64 `gorm:"column:user_log_id;primary_key;AUTO_INCREMENT" json:"user_log_id"` 11 | UserID int64 `json:"user_id"` 12 | IP string `json:"ip"` 13 | UA string `json:"ua"` 14 | CreateTime string `json:"create_time"` 15 | User User `gorm:"foreignKey:user_id;association_foreignkey:user_id" json:"user"` 16 | } 17 | 18 | // 创建日志 19 | func (ul UserLog) Create() (id int64, err error) { 20 | ul.CreateTime = utils.GetCurrntTime() 21 | err = orm.Eloquent.Table("user_logs").Create(&ul).Error 22 | id = ul.UserLogID 23 | return 24 | } 25 | 26 | // 删除日志 27 | func (ul UserLog) Delete(userLogIds []int64)(err error) { 28 | err = orm.Eloquent.Table("user_logs").Where("user_log_id in (?)",userLogIds).Delete(&ul).Error 29 | return 30 | } 31 | 32 | // 获取日志列表 33 | func (ul UserLog) GetUserLogPage(pageSize int, pageIndex int, date []string) (userLogs []UserLog,count int64,err error) { 34 | table := orm.Eloquent.Model(userLogs).Preload("User") 35 | if len(date) > 0 && date[0] != "" { 36 | table = table.Where("create_time >= ?",date[0]) 37 | } 38 | if len(date) > 0 && date[1] != "" { 39 | table = table.Where("create_time <= ?",date[1]) 40 | } 41 | if err = table.Offset((pageIndex -1) * pageSize).Limit(pageSize).Order("user_logs.create_time desc").Find(&userLogs).Error; err != nil { 42 | if err == gorm.ErrRecordNotFound { 43 | err = nil 44 | } 45 | } 46 | table.Count(&count) 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /server/models/user_role.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | orm "go-element-admin/db" 5 | "go-element-admin/utils" 6 | ) 7 | 8 | type UserRole struct { 9 | UserRoleID int64 `gorm:"column:user_role_id;primary_key;AUTO_INCREMENT" json:"user_role_id"` 10 | UserId int64 `gorm:"column:user_id;" json:"user_id"` 11 | RoleId int64 `gorm:"column:role_id;" json:"role_id"` 12 | CreateTime string `gorm:"column:create_time" json:"create_time"` 13 | } 14 | 15 | // 创建用户角色 16 | func (ur UserRole) Create() (id int64, err error) { 17 | ur.CreateTime = utils.GetCurrntTime() 18 | err = orm.Eloquent.Table("user_roles").Create(&ur).Error 19 | id = ur.UserRoleID 20 | return 21 | } 22 | 23 | 24 | // 删除用户角色 25 | func (ur UserRole) Delete(userId int64,roleIds []int64)(err error) { 26 | err = orm.Eloquent.Table("user_roles").Where("user_id = ?",userId).Where("role_id in (?)",roleIds).Delete(&ur).Error 27 | return 28 | } 29 | 30 | -------------------------------------------------------------------------------- /server/routers/router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | _ "go-element-admin/docs" 5 | "github.com/gin-gonic/gin" 6 | ginSwagger "github.com/swaggo/gin-swagger" 7 | "github.com/swaggo/gin-swagger/swaggerFiles" 8 | "go-element-admin/apis" 9 | "go-element-admin/middlewares" 10 | ) 11 | 12 | func InitRouter() *gin.Engine { 13 | r := gin.New() 14 | 15 | r.Use(Cors()) 16 | 17 | r.GET("/chat", apis.Chat) 18 | r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 19 | r.POST("/user/login", apis.UserLogin) 20 | 21 | auth := r.Group("/") 22 | auth.Use(middlewares.JwtMiddleWare()) 23 | { 24 | auth.GET("/user/info", apis.UserInfo) 25 | auth.Use(middlewares.RoleCasbinMiddleWare()) 26 | { 27 | // 用户管理 28 | auth.GET("/users", apis.UserList) 29 | auth.DELETE("/users", apis.DeleteUser) 30 | user := auth.Group("user") 31 | { 32 | user.POST("", apis.CreateUser) 33 | user.PUT("/:user_id", apis.UpdateUser) 34 | user.POST("/pwd/:user_id", apis.UpdatePwdUser) 35 | user.GET("/logs", apis.UserLogList) 36 | user.DELETE("/logs", apis.DeleteUserLog) 37 | } 38 | 39 | // 角色管理 40 | auth.GET("/roles", apis.RoleList) 41 | auth.DELETE("/roles", apis.DeleteRole) 42 | role := auth.Group("role") 43 | { 44 | role.POST("", apis.CreateRole) 45 | role.PUT("/:role_id", apis.UpdateRole) 46 | } 47 | 48 | // 接口管理 49 | auth.GET("/paths", apis.PathList) 50 | auth.DELETE("/paths", apis.DeletePath) 51 | path := auth.Group("path") 52 | { 53 | path.POST("", apis.CreatePath) 54 | path.GET("/:path_id", apis.GetPath) 55 | path.PUT("/:path_id", apis.UpdatePath) 56 | } 57 | 58 | // 角色管理 59 | auth.GET("/menus", apis.MenuList) 60 | auth.DELETE("/menus", apis.DeleteMenu) 61 | menu := auth.Group("menu") 62 | { 63 | menu.POST("", apis.CreateMenu) 64 | menu.GET("/:menu_id", apis.GetMenu) 65 | menu.PUT("/:menu_id", apis.UpdateMenu) 66 | } 67 | } 68 | 69 | } 70 | return r 71 | } 72 | 73 | 74 | func Cors() gin.HandlerFunc { 75 | return func(c *gin.Context) { 76 | c.Header("Access-Control-Allow-Origin", "*") 77 | c.Next() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /server/utils/gcasbin/gcasbin.go: -------------------------------------------------------------------------------- 1 | package gcasbin 2 | 3 | import ( 4 | config "go-element-admin/configs" 5 | "github.com/casbin/casbin/v2" 6 | "github.com/casbin/casbin/v2/model" 7 | gormadapter "github.com/casbin/gorm-adapter/v3" 8 | "strconv" 9 | ) 10 | 11 | var Enforcer *casbin.SyncedEnforcer 12 | 13 | var text = ` 14 | [request_definition] 15 | r = sub, obj, act 16 | 17 | [policy_definition] 18 | p = sub, obj, act 19 | 20 | [policy_effect] 21 | e = some(where (p.eft == allow)) 22 | 23 | [matchers] 24 | m = r.sub == p.sub && (keyMatch2(r.obj, p.obj) || keyMatch(r.obj, p.obj)) && (r.act == p.act || p.act == "*") 25 | ` 26 | 27 | func init() { 28 | conn := config.DatabaseConfig.Username + ":" + config.DatabaseConfig.Password + "@tcp(" + config.DatabaseConfig.Host + ":" + strconv.Itoa(config.DatabaseConfig.Port) + ")/" + config.DatabaseConfig.Database 29 | Apter, err := gormadapter.NewAdapter(config.DatabaseConfig.Dbtype, conn, true) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | m, err := model.NewModelFromString(text) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | Enforcer, err = casbin.NewSyncedEnforcer(m, Apter) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | // 开启权限认证日志 45 | Enforcer.EnableLog(true) 46 | 47 | // 加载策略 48 | err = Enforcer.LoadPolicy() 49 | if err != nil { 50 | panic(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server/utils/jwt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "go-element-admin/configs" 5 | jwt "github.com/dgrijalva/jwt-go" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | var jwtSecret = []byte(configs.ApplicationConfig.JwtSecret) 11 | 12 | type Claims struct { 13 | UserId int64 `json:"user_id"` 14 | UserName string `json:"user_name"` 15 | Roles []int64 `json:"roles"` 16 | jwt.StandardClaims 17 | } 18 | 19 | type JwtDo struct { 20 | } 21 | 22 | 23 | func (JwtDo) GenerateToken(userId int64, userName string, roles []int64) (string, error) { 24 | nowTime := time.Now() 25 | expireTime := nowTime.Add(3 * time.Hour) 26 | claims := Claims{ 27 | userId, 28 | userName, 29 | roles, 30 | jwt.StandardClaims { 31 | ExpiresAt : expireTime.Unix(), 32 | Issuer : "bigfool", 33 | }, 34 | } 35 | 36 | tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 37 | token, err := tokenClaims.SignedString(jwtSecret) 38 | var build strings.Builder 39 | build.WriteString("Bearer ") 40 | build.WriteString(token) 41 | token = build.String() 42 | return token, err 43 | } 44 | 45 | func (JwtDo) ParseToken(token string) (*Claims, error) { 46 | if strings.Contains(token,"Bearer ") { 47 | token = strings.Replace(token,"Bearer ","",1) 48 | } 49 | tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) { 50 | return jwtSecret, nil 51 | }) 52 | 53 | if tokenClaims != nil { 54 | if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { 55 | return claims, nil 56 | } 57 | } 58 | 59 | return nil, err 60 | } 61 | 62 | var Jwt = new(JwtDo) 63 | -------------------------------------------------------------------------------- /server/utils/log.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "go-element-admin/configs" 5 | "fmt" 6 | "github.com/sirupsen/logrus" 7 | "io" 8 | "log" 9 | "os" 10 | "path" 11 | "time" 12 | ) 13 | 14 | func NewLogger() *logrus.Logger { 15 | logr := logrus.New() 16 | return logr 17 | } 18 | 19 | func DefaultLogger(debug bool) *logrus.Logger { 20 | lgr := NewLogger() 21 | 22 | writerSto := os.Stdout 23 | 24 | date := time.Now().Format("2006-01-02") 25 | name := path.Join(configs.ApplicationConfig.LogPath,fmt.Sprintf("%s.%s",date,"log")) 26 | writerFile, err := os.OpenFile(name,os.O_WRONLY|os.O_CREATE,0666) 27 | if err != nil { 28 | log.Fatalf("create file log failed: %v",err) 29 | } 30 | 31 | if debug { 32 | lgr.SetLevel(logrus.DebugLevel) 33 | } else { 34 | lgr.SetLevel(logrus.InfoLevel) 35 | } 36 | 37 | lgr.SetOutput(io.MultiWriter(writerSto,writerFile)) 38 | 39 | lgr.SetFormatter(&logrus.TextFormatter{}) 40 | 41 | return lgr 42 | } 43 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/api/system/menu.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listMenu(params) { 4 | return request({ 5 | url: '/menus', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function getMenu(menu_id) { 12 | return request({ 13 | url: '/menu/' + menu_id, 14 | method: 'get' 15 | }) 16 | } 17 | 18 | export function delMenu(data) { 19 | return request({ 20 | url: '/menus', 21 | method: 'delete', 22 | data 23 | }) 24 | } 25 | 26 | export function addMenu(data) { 27 | return request({ 28 | url: '/menu', 29 | method: 'post', 30 | data 31 | }) 32 | } 33 | 34 | export function updateMenu(menu_id, data) { 35 | return request({ 36 | url: '/menu/' + menu_id, 37 | method: 'put', 38 | data 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/api/system/path.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listPath(params) { 4 | return request({ 5 | url: '/paths', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function getPath(path_id) { 12 | return request({ 13 | url: '/path/' + path_id, 14 | method: 'get' 15 | }) 16 | } 17 | 18 | export function delPath(data) { 19 | return request({ 20 | url: '/paths', 21 | method: 'delete', 22 | data 23 | }) 24 | } 25 | 26 | export function addPath(data) { 27 | return request({ 28 | url: '/path', 29 | method: 'post', 30 | data 31 | }) 32 | } 33 | 34 | export function updatePath(path_id, data) { 35 | return request({ 36 | url: '/path/' + path_id, 37 | method: 'put', 38 | data 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/api/system/role.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listRole(params) { 4 | return request({ 5 | url: '/roles', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function getRole(params) { 12 | return request({ 13 | url: '/role/get', 14 | method: 'get', 15 | params 16 | }) 17 | } 18 | 19 | export function delRole(data) { 20 | return request({ 21 | url: '/roles', 22 | method: 'delete', 23 | data 24 | }) 25 | } 26 | 27 | export function addRole(data) { 28 | return request({ 29 | url: '/role', 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function updateRole(role_id, data) { 36 | return request({ 37 | url: '/role/' + role_id, 38 | method: 'put', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/system/user-log.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listLog(params) { 4 | return request({ 5 | url: '/user/logs', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function delLog(data) { 12 | return request({ 13 | url: '/user/logs', 14 | method: 'DELETE', 15 | data 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/api/system/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listUser(params) { 4 | return request({ 5 | url: '/users', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function addUser(data) { 12 | return request({ 13 | url: '/user', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | 19 | export function delUser(data) { 20 | return request({ 21 | url: '/users', 22 | method: 'DELETE', 23 | data 24 | }) 25 | } 26 | 27 | export function updateUser(user_id, data) { 28 | return request({ 29 | url: '/user/' + user_id, 30 | method: 'put', 31 | data 32 | }) 33 | } 34 | 35 | export function updatePwd(user_id, data) { 36 | return request({ 37 | url: '/user/pwd/' + user_id, 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/user/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/user/info', 14 | method: 'get' 15 | }) 16 | } 17 | 18 | export function logout() { 19 | return request({ 20 | url: '/user/logout', 21 | method: 'post' 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/403_images/403.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigfool-cn/go-element-admin/3297eb1cc7f0f1c3149e16d5d59d76fb6e37b4ae/src/assets/403_images/403.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigfool-cn/go-element-admin/3297eb1cc7f0f1c3149e16d5d59d76fb6e37b4ae/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigfool-cn/go-element-admin/3297eb1cc7f0f1c3149e16d5d59d76fb6e37b4ae/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigfool-cn/go-element-admin/3297eb1cc7f0f1c3149e16d5d59d76fb6e37b4ae/src/assets/custom-theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigfool-cn/go-element-admin/3297eb1cc7f0f1c3149e16d5d59d76fb6e37b4ae/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /src/assets/pay_wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigfool-cn/go-element-admin/3297eb1cc7f0f1c3149e16d5d59d76fb6e37b4ae/src/assets/pay_wechat.png -------------------------------------------------------------------------------- /src/assets/pay_zfb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigfool-cn/go-element-admin/3297eb1cc7f0f1c3149e16d5d59d76fb6e37b4ae/src/assets/pay_zfb.png -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 68 | 69 | 82 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/components/IconSelect/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 44 | 45 | 69 | -------------------------------------------------------------------------------- /src/components/IconSelect/requireIcons.js: -------------------------------------------------------------------------------- 1 | const req = require.context('../../icons/svg', false, /\.svg$/) 2 | const requireAll = requireContext => requireContext.keys() 3 | 4 | const re = /\.\/(.*)\.svg/ 5 | 6 | const icons = requireAll(req).map(i => { 7 | return i.match(re)[1] 8 | }) 9 | 10 | export default icons 11 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /src/components/SizeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 57 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 46 | 47 | 62 | -------------------------------------------------------------------------------- /src/components/TextHoverEffect/Mallki.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 114 | -------------------------------------------------------------------------------- /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/directive/permission/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | export default { 4 | inserted(el, binding, vnode) { 5 | const { value } = binding 6 | const buttons = store.getters && store.getters.buttons 7 | 8 | if (value && value instanceof Array && value.length > 0) { 9 | const permissionButtons = value 10 | 11 | const hasPermission = buttons.some(button => { 12 | return permissionButtons.includes(button) 13 | }) 14 | 15 | if (!hasPermission) { 16 | el.parentNode && el.parentNode.removeChild(el) 17 | } 18 | } else { 19 | throw new Error(`需要权限标识,例: v-permission="['system:menu:query','system:menu:add']"`) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | // import parseTime, formatTime and set to filter 2 | export { parseTime, formatTime } from '@/utils' 3 | 4 | /** 5 | * Show plural label if time is plural number 6 | * @param {number} time 7 | * @param {string} label 8 | * @return {string} 9 | */ 10 | function pluralize(time, label) { 11 | if (time === 1) { 12 | return time + label 13 | } 14 | return time + label + 's' 15 | } 16 | 17 | /** 18 | * @param {number} time 19 | */ 20 | export function timeAgo(time) { 21 | const between = Date.now() / 1000 - Number(time) 22 | if (between < 3600) { 23 | return pluralize(~~(between / 60), ' minute') 24 | } else if (between < 86400) { 25 | return pluralize(~~(between / 3600), ' hour') 26 | } else { 27 | return pluralize(~~(between / 86400), ' day') 28 | } 29 | } 30 | 31 | /** 32 | * Number formatting 33 | * like 10000 => 10k 34 | * @param {number} num 35 | * @param {number} digits 36 | */ 37 | export function numberFormatter(num, digits) { 38 | const si = [ 39 | { value: 1E18, symbol: 'E' }, 40 | { value: 1E15, symbol: 'P' }, 41 | { value: 1E12, symbol: 'T' }, 42 | { value: 1E9, symbol: 'G' }, 43 | { value: 1E6, symbol: 'M' }, 44 | { value: 1E3, symbol: 'k' } 45 | ] 46 | for (let i = 0; i < si.length; i++) { 47 | if (num >= si[i].value) { 48 | return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol 49 | } 50 | } 51 | return num.toString() 52 | } 53 | 54 | /** 55 | * 10000 => "10,000" 56 | * @param {number} num 57 | */ 58 | export function toThousandFilter(num) { 59 | return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ',')) 60 | } 61 | 62 | /** 63 | * Upper case first char 64 | * @param {String} string 65 | */ 66 | export function uppercaseFirst(string) { 67 | return string.charAt(0).toUpperCase() + string.slice(1) 68 | } 69 | -------------------------------------------------------------------------------- /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/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/build.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/icons/svg/cascader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/checkbox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/date-range.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dict.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/druid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/input.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/job.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/log.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/logininfor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/monitor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/phone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/post.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/question.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/radio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/rate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/row.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/select.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/server.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/slider.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/swagger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/switch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/system.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/textarea.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/time-range.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/time.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tool.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/validCode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 49 | 50 | 58 | -------------------------------------------------------------------------------- /src/layout/components/Settings/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 92 | 93 | 118 | -------------------------------------------------------------------------------- /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 | this.fixBugIniOS() 10 | }, 11 | methods: { 12 | fixBugIniOS() { 13 | const $subMenu = this.$refs.subMenu 14 | if ($subMenu) { 15 | const handleMouseleave = $subMenu.handleMouseleave 16 | $subMenu.handleMouseleave = (e) => { 17 | if (this.device === 'mobile') { 18 | return 19 | } 20 | handleMouseleave(e) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 55 | -------------------------------------------------------------------------------- /src/layout/components/TagsView/ScrollPane.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 78 | 79 | 95 | -------------------------------------------------------------------------------- /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/layout/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | 60 | 103 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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 | // import enLang from 'element-ui/lib/locale/lang/zh-CN'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖 10 | 11 | import '@/styles/index.scss' // global css 12 | 13 | import App from './App' 14 | import store from './store' 15 | import router from './router' 16 | import permission from './directive/permission' 17 | 18 | import './icons' // icon 19 | import './permission' // permission control 20 | 21 | import * as filters from './filters' // global filters 22 | 23 | import { parseTime, resetForm } from '@/utils/costum' 24 | // 全局方法挂载 25 | Vue.prototype.parseTime = parseTime 26 | Vue.prototype.resetForm = resetForm 27 | 28 | Vue.prototype.msgSuccess = function(msg) { 29 | this.$message({ showClose: true, message: msg, type: 'success' }) 30 | } 31 | 32 | Vue.prototype.msgError = function(msg) { 33 | this.$message({ showClose: true, message: msg, type: 'error' }) 34 | } 35 | 36 | Vue.prototype.msgInfo = function(msg) { 37 | this.$message.info(msg) 38 | } 39 | 40 | Vue.use(permission) 41 | 42 | Vue.use(Element, { 43 | size: Cookies.get('size') || 'medium' // set element-ui default size 44 | }) 45 | 46 | // register global utility filters 47 | Object.keys(filters).forEach(key => { 48 | Vue.filter(key, filters[key]) 49 | }) 50 | 51 | Vue.config.productionTip = false 52 | 53 | new Vue({ 54 | el: '#app', 55 | router, 56 | store, 57 | render: h => h(App) 58 | }) 59 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() 28 | } else { 29 | // determine whether the user has obtained his permission roles through getInfo 30 | const hasRoles = store.getters.roles && store.getters.roles.length > 0 31 | if (hasRoles) { 32 | next() 33 | } else { 34 | try { 35 | // get user info 36 | // note: roles must be a object array! such as: ['admin'] or ,['developer','editor'] 37 | const { menus } = await store.dispatch('user/getInfo') 38 | // generate accessible routes map based on roles 39 | const accessRoutes = await store.dispatch('permission/generateRoutes', menus) 40 | // dynamically add accessible routes 41 | router.addRoutes(accessRoutes) 42 | 43 | // hack method to ensure that addRoutes is complete 44 | // set the replace: true, so the navigation will not leave a history record 45 | next({ ...to, replace: true }) 46 | } catch (error) { 47 | // remove token and go to login page to re-login 48 | await store.dispatch('user/resetToken') 49 | Message.error(error || 'Has Error') 50 | next(`/login?redirect=${to.path}`) 51 | NProgress.done() 52 | } 53 | } 54 | } 55 | } else { 56 | /* has no token*/ 57 | 58 | if (whiteList.indexOf(to.path) !== -1) { 59 | // in the free login whitelist, go directly 60 | next() 61 | } else { 62 | // other pages that do not have permission to access are redirected to the login page. 63 | next(`/login?redirect=${to.path}`) 64 | NProgress.done() 65 | } 66 | } 67 | }) 68 | 69 | router.afterEach(() => { 70 | // finish progress bar 71 | NProgress.done() 72 | }) 73 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | /* Layout */ 7 | import Layout from '@/layout' 8 | 9 | /** 10 | * constantRoutes 11 | * a base page that does not have permission requirements 12 | * all roles can be accessed 13 | */ 14 | export const constantRoutes = [ 15 | { 16 | path: '/redirect', 17 | component: Layout, 18 | hidden: true, 19 | children: [ 20 | { 21 | path: '/redirect/:path(.*)', 22 | component: () => import('@/views/redirect/index') 23 | } 24 | ] 25 | }, 26 | { 27 | path: '/login', 28 | component: () => import('@/views/login/index'), 29 | hidden: true 30 | }, 31 | { 32 | path: '/auth-redirect', 33 | component: () => import('@/views/login/auth-redirect'), 34 | hidden: true 35 | }, 36 | { 37 | path: '/', 38 | component: Layout, 39 | redirect: '/dashboard', 40 | children: [ 41 | { 42 | path: 'dashboard', 43 | component: () => import('@/views/dashboard/index'), 44 | name: 'Dashboard', 45 | meta: { title: '首页', icon: 'dashboard', affix: true } 46 | } 47 | ] 48 | } 49 | ] 50 | 51 | /** 52 | * errorRoutes 53 | */ 54 | export const errorRoutes = [ 55 | { 56 | path: '/error', 57 | component: Layout, 58 | redirect: 'noRedirect', 59 | name: 'ErrorPages', 60 | meta: { 61 | title: '错误页面', 62 | icon: '404' 63 | }, 64 | hidden: true, 65 | children: [ 66 | { 67 | path: '403', 68 | component: () => import('@/views/error-page/403'), 69 | name: 'Page403', 70 | meta: { title: '禁止访问', noCache: true } 71 | }, 72 | { 73 | path: '404', 74 | component: () => import('@/views/error-page/404'), 75 | name: 'Page404', 76 | meta: { title: '无法找到页面', noCache: true } 77 | } 78 | ] 79 | }, 80 | // 404 page must be placed at the end !!! 81 | { path: '*', redirect: '/error/404', hidden: true } 82 | ] 83 | 84 | const createRouter = () => new Router({ 85 | // mode: 'history', // require service support 86 | scrollBehavior: () => ({ y: 0 }), 87 | routes: constantRoutes 88 | }) 89 | 90 | const router = createRouter() 91 | 92 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 93 | export function resetRouter() { 94 | const newRouter = createRouter() 95 | router.matcher = newRouter.matcher // reset router 96 | } 97 | 98 | export default router 99 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Go Element Admin', 3 | 4 | /** 5 | * @type {boolean} true | false 6 | * @description Whether show the settings right-panel 7 | */ 8 | showSettings: true, 9 | 10 | /** 11 | * @type {boolean} true | false 12 | * @description Whether need tagsView 13 | */ 14 | tagsView: true, 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: true, 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/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 | user_id: state => state.user.user_id, 10 | name: state => state.user.name, 11 | menus: state => state.user.menus, 12 | buttons: state => state.user.buttons, 13 | roles: state => state.user.roles, 14 | permission_routes: state => state.permission.routes, 15 | chat_name: state => state.chat.chat_name, 16 | chat_names: state => state.chat.chat_names, 17 | chat_msgs: state => state.chat.chat_msgs 18 | } 19 | export default getters 20 | -------------------------------------------------------------------------------- /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/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop', 9 | size: Cookies.get('size') || 'medium' 10 | } 11 | 12 | const mutations = { 13 | TOGGLE_SIDEBAR: state => { 14 | state.sidebar.opened = !state.sidebar.opened 15 | state.sidebar.withoutAnimation = false 16 | if (state.sidebar.opened) { 17 | Cookies.set('sidebarStatus', 1) 18 | } else { 19 | Cookies.set('sidebarStatus', 0) 20 | } 21 | }, 22 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 23 | Cookies.set('sidebarStatus', 0) 24 | state.sidebar.opened = false 25 | state.sidebar.withoutAnimation = withoutAnimation 26 | }, 27 | TOGGLE_DEVICE: (state, device) => { 28 | state.device = device 29 | }, 30 | SET_SIZE: (state, size) => { 31 | state.size = size 32 | Cookies.set('size', size) 33 | } 34 | } 35 | 36 | const actions = { 37 | toggleSideBar({ commit }) { 38 | commit('TOGGLE_SIDEBAR') 39 | }, 40 | closeSideBar({ commit }, { withoutAnimation }) { 41 | commit('CLOSE_SIDEBAR', withoutAnimation) 42 | }, 43 | toggleDevice({ commit }, device) { 44 | commit('TOGGLE_DEVICE', device) 45 | }, 46 | setSize({ commit }, size) { 47 | commit('SET_SIZE', size) 48 | } 49 | } 50 | 51 | export default { 52 | namespaced: true, 53 | state, 54 | mutations, 55 | actions 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/chat.js: -------------------------------------------------------------------------------- 1 | import { Message } from 'element-ui' 2 | 3 | const state = { 4 | chat_name: '', 5 | chat_names: [], 6 | chat_msgs: [] 7 | } 8 | 9 | const mutations = { 10 | CHAT_NAME: (state, name) => { 11 | state.chat_name = name 12 | }, 13 | CHAT_NAMES: (state, names) => { 14 | state.chat_names = names 15 | }, 16 | PUSH_CHAT_MSG: (state, msg) => { 17 | state.chat_msgs.push(msg) 18 | if (state.chat_msgs.length >= 1200) { 19 | state.chat_msgs = state.chat_msgs.slice(600) 20 | } 21 | }, 22 | DEL_CHAT_DATA: (state) => { 23 | state.chat_name = '' 24 | state.chat_names = [] 25 | state.chat_msgs = [] 26 | } 27 | } 28 | 29 | const actions = { 30 | delChatData({ commit }) { 31 | commit('DEL_CHAT_DATA') 32 | }, 33 | pushChatMsg({ commit }, userMsg) { 34 | if (userMsg.type_id !== undefined) { 35 | let msg = {} 36 | switch (userMsg.type_id) { 37 | case -1: 38 | return Message.error(userMsg.msg) 39 | case 0: 40 | msg = { 41 | type_id: 0, 42 | name: userMsg.name, 43 | time: userMsg.time, 44 | msg: userMsg.msg 45 | } 46 | commit('CHAT_NAMES', userMsg.names) 47 | commit('PUSH_CHAT_MSG', msg) 48 | break 49 | case 1: 50 | commit('CHAT_NAME', userMsg.msg) 51 | break 52 | case 12: 53 | msg = { 54 | type_id: 12, 55 | name: userMsg.name, 56 | time: userMsg.time, 57 | msg: userMsg.msg 58 | } 59 | commit('PUSH_CHAT_MSG', msg) 60 | break 61 | } 62 | } 63 | } 64 | } 65 | 66 | export default { 67 | namespaced: true, 68 | state, 69 | mutations, 70 | actions 71 | } 72 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { errorRoutes, constantRoutes } from '@/router' 2 | import Layout from '@/layout' 3 | 4 | /** 5 | * Filter asynchronous routing tables by recursion 6 | * @param routes asyncRoutes 7 | * @param roles 8 | */ 9 | export function filterAsyncRoutes(routes) { 10 | const res = [] 11 | 12 | routes.forEach(route => { 13 | const component = route.component 14 | const tmp = { 15 | path: route.path, 16 | component: route.component === 'Layout' ? Layout : () => import(`@/views${component}`), // resolve => require([`@/views${component}`], resolve), 17 | redirect: route.redirect || undefined, 18 | hidden: !!route.hidden, 19 | name: route.name, 20 | meta: {}, 21 | children: route.children || undefined 22 | } 23 | tmp.meta.title = route.title 24 | if (route.icon) { 25 | tmp.meta.icon = route.icon 26 | } 27 | if (tmp.children) { 28 | if (tmp.children.length) { 29 | tmp.alwaysShow = true 30 | } 31 | tmp.children = filterAsyncRoutes(tmp.children) 32 | } 33 | res.push(tmp) 34 | }) 35 | return res 36 | } 37 | 38 | const state = { 39 | routes: [], 40 | addRoutes: [] 41 | } 42 | 43 | const mutations = { 44 | SET_ROUTES: (state, routes) => { 45 | state.addRoutes = routes 46 | state.routes = constantRoutes.concat(routes) 47 | } 48 | } 49 | 50 | const actions = { 51 | generateRoutes({ commit }, menus) { 52 | return new Promise(resolve => { 53 | const accessedRoutes = filterAsyncRoutes(menus).concat(errorRoutes) 54 | commit('SET_ROUTES', accessedRoutes) 55 | resolve(accessedRoutes) 56 | }) 57 | } 58 | } 59 | 60 | export default { 61 | namespaced: true, 62 | state, 63 | mutations, 64 | actions 65 | } 66 | -------------------------------------------------------------------------------- /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 | if (state.hasOwnProperty(key)) { 17 | state[key] = value 18 | } 19 | } 20 | } 21 | 22 | const actions = { 23 | changeSetting({ commit }, data) { 24 | commit('CHANGE_SETTING', data) 25 | } 26 | } 27 | 28 | export default { 29 | namespaced: true, 30 | state, 31 | mutations, 32 | actions 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/styles/btn.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | @mixin colorBtn($color) { 4 | background: $color; 5 | 6 | &:hover { 7 | color: $color; 8 | 9 | &:before, 10 | &:after { 11 | background: $color; 12 | } 13 | } 14 | } 15 | 16 | .blue-btn { 17 | @include colorBtn($blue) 18 | } 19 | 20 | .light-blue-btn { 21 | @include colorBtn($light-blue) 22 | } 23 | 24 | .red-btn { 25 | @include colorBtn($red) 26 | } 27 | 28 | .pink-btn { 29 | @include colorBtn($pink) 30 | } 31 | 32 | .green-btn { 33 | @include colorBtn($green) 34 | } 35 | 36 | .tiffany-btn { 37 | @include colorBtn($tiffany) 38 | } 39 | 40 | .yellow-btn { 41 | @include colorBtn($yellow) 42 | } 43 | 44 | .pan-btn { 45 | font-size: 14px; 46 | color: #fff; 47 | padding: 14px 36px; 48 | border-radius: 8px; 49 | border: none; 50 | outline: none; 51 | transition: 600ms ease all; 52 | position: relative; 53 | display: inline-block; 54 | 55 | &:hover { 56 | background: #fff; 57 | 58 | &:before, 59 | &:after { 60 | width: 100%; 61 | transition: 600ms ease all; 62 | } 63 | } 64 | 65 | &:before, 66 | &:after { 67 | content: ''; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | height: 2px; 72 | width: 0; 73 | transition: 400ms ease all; 74 | } 75 | 76 | &::after { 77 | right: inherit; 78 | top: inherit; 79 | left: 0; 80 | bottom: 0; 81 | } 82 | } 83 | 84 | .custom-button { 85 | display: inline-block; 86 | line-height: 1; 87 | white-space: nowrap; 88 | cursor: pointer; 89 | background: #fff; 90 | color: #fff; 91 | -webkit-appearance: none; 92 | text-align: center; 93 | box-sizing: border-box; 94 | outline: 0; 95 | margin: 0; 96 | padding: 10px 15px; 97 | font-size: 14px; 98 | border-radius: 4px; 99 | } 100 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | .cell { 19 | .el-tag { 20 | margin-right: 0px; 21 | } 22 | } 23 | 24 | .small-padding { 25 | .cell { 26 | padding-left: 5px; 27 | padding-right: 5px; 28 | } 29 | } 30 | 31 | .fixed-width { 32 | .el-button--mini { 33 | padding: 7px 10px; 34 | min-width: 60px; 35 | } 36 | } 37 | 38 | .status-col { 39 | .cell { 40 | padding: 0 10px; 41 | text-align: center; 42 | 43 | .el-tag { 44 | margin-right: 0px; 45 | } 46 | } 47 | } 48 | 49 | // to fixed https://github.com/ElemeFE/element/issues/2461 50 | .el-dialog { 51 | transform: none; 52 | left: 0; 53 | position: relative; 54 | margin: 0 auto; 55 | } 56 | 57 | // refine element ui upload 58 | .upload-container { 59 | .el-upload { 60 | width: 100%; 61 | 62 | .el-upload-dragger { 63 | width: 100%; 64 | height: 200px; 65 | } 66 | } 67 | } 68 | 69 | // dropdown 70 | .el-dropdown-menu { 71 | a { 72 | display: block 73 | } 74 | } 75 | 76 | // fix date-picker ui bug in filter-item 77 | .el-range-editor.el-input__inner { 78 | display: inline-flex !important; 79 | } 80 | 81 | // to fix el-date-picker css style 82 | .el-range-separator { 83 | box-sizing: content-box; 84 | } 85 | 86 | .el-tooltip__popper{ 87 | max-width:50% 88 | } 89 | -------------------------------------------------------------------------------- /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/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | @mixin pct($pct) { 31 | width: #{$pct}; 32 | position: relative; 33 | margin: 0 auto; 34 | } 35 | 36 | @mixin triangle($width, $height, $color, $direction) { 37 | $width: $width/2; 38 | $color-border-style: $height solid $color; 39 | $transparent-border-style: $width solid transparent; 40 | height: 0; 41 | width: 0; 42 | 43 | @if $direction==up { 44 | border-bottom: $color-border-style; 45 | border-left: $transparent-border-style; 46 | border-right: $transparent-border-style; 47 | } 48 | 49 | @else if $direction==right { 50 | border-left: $color-border-style; 51 | border-top: $transparent-border-style; 52 | border-bottom: $transparent-border-style; 53 | } 54 | 55 | @else if $direction==down { 56 | border-top: $color-border-style; 57 | border-left: $transparent-border-style; 58 | border-right: $transparent-border-style; 59 | } 60 | 61 | @else if $direction==left { 62 | border-right: $color-border-style; 63 | border-top: $transparent-border-style; 64 | border-bottom: $transparent-border-style; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'Admin-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 | -------------------------------------------------------------------------------- /src/utils/costum.js: -------------------------------------------------------------------------------- 1 | // 日期格式化 2 | export function parseTime(time, pattern) { 3 | if (arguments.length === 0 || !time) { 4 | return null 5 | } 6 | const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' 7 | let date 8 | if (typeof time === 'object') { 9 | date = time 10 | } else { 11 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 12 | time = parseInt(time) 13 | } 14 | if ((typeof time === 'number') && (time.toString().length === 10)) { 15 | time = time * 1000 16 | } 17 | date = new Date(time) 18 | } 19 | const formatObj = { 20 | y: date.getFullYear(), 21 | m: date.getMonth() + 1, 22 | d: date.getDate(), 23 | h: date.getHours(), 24 | i: date.getMinutes(), 25 | s: date.getSeconds(), 26 | a: date.getDay() 27 | } 28 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 29 | let value = formatObj[key] 30 | // Note: getDay() returns 0 on Sunday 31 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] } 32 | if (result.length > 0 && value < 10) { 33 | value = '0' + value 34 | } 35 | return value || 0 36 | }) 37 | return time_str 38 | } 39 | 40 | // 表单重置 41 | export function resetForm(refName) { 42 | if (this.$refs[refName]) { 43 | this.$refs[refName].resetFields() 44 | } 45 | } 46 | 47 | // 添加日期范围 48 | export function addDateRange(params, dateRange) { 49 | var search = params 50 | search.beginTime = '' 51 | search.endTime = '' 52 | if (dateRange != null && dateRange !== '') { 53 | search.beginTime = this.dateRange[0] 54 | search.endTime = this.dateRange[1] 55 | } 56 | return search 57 | } 58 | 59 | // 回显数据字典 60 | export function selectDictLabel(datas, value) { 61 | var actions = [] 62 | Object.keys(datas).map((key) => { 63 | if (datas[key].dictValue === ('' + value)) { 64 | actions.push(datas[key].dictLabel) 65 | return false 66 | } 67 | }) 68 | return actions.join('') 69 | } 70 | 71 | // 字符串格式化(%s ) 72 | export function sprintf(str) { 73 | var args = arguments; var flag = true; var i = 1 74 | str = str.replace(/%s/g, function() { 75 | var arg = args[i++] 76 | if (typeof arg === 'undefined') { 77 | flag = false 78 | return '' 79 | } 80 | return arg 81 | }) 82 | return flag ? str : '' 83 | } 84 | 85 | // 转换字符串,undefined,null等转化为"" 86 | export function praseStrEmpty(str) { 87 | if (!str || str === 'undefined' || str === 'null') { 88 | return '' 89 | } 90 | return str 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Go Element Admin' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/open-window.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Sting} url 3 | * @param {Sting} title 4 | * @param {Number} w 5 | * @param {Number} h 6 | */ 7 | export default function openWindow(url, title, w, h) { 8 | // Fixes dual-screen position Most browsers Firefox 9 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left 10 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top 11 | 12 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width 13 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height 14 | 15 | const left = ((width / 2) - (w / 2)) + dualScreenLeft 16 | const top = ((height / 2) - (h / 2)) + dualScreenTop 17 | 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) 18 | 19 | // Puts focus on the newWindow 20 | if (window.focus) { 21 | newWindow.focus() 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /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 | 17 | if (!hasPermission) { 18 | return false 19 | } 20 | return true 21 | } else { 22 | console.error(`need roles! Like v-permission="['admin','editor']"`) 23 | return false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | // withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 5000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['Authorization'] = getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | return Promise.reject(error) 29 | } 30 | ) 31 | 32 | // response interceptor 33 | service.interceptors.response.use( 34 | /** 35 | * If you want to get http information such as headers or status 36 | * Please return response => response 37 | */ 38 | 39 | /** 40 | * Determine the request status by custom code 41 | * Here is just an example 42 | * You can also judge the status by HTTP Status Code 43 | */ 44 | response => { 45 | const res = response.data 46 | // if the custom code is not 20000, it is judged as an error. 47 | if (res.code !== 0) { 48 | Message({ 49 | message: res.message || 'Error', 50 | type: 'error', 51 | duration: 5 * 1000 52 | }) 53 | return Promise.reject(new Error(res.message || 'Error')) 54 | } else { 55 | return res 56 | } 57 | }, 58 | error => { 59 | // 401: Illegal token: Other clients logged in: Token expired; 60 | if (error.response.status === 401) { 61 | // to re-login 62 | MessageBox.confirm('登陆已过期,请重新登录', '重新登录', { 63 | confirmButtonText: '重新登录', 64 | cancelButtonText: '取消', 65 | type: 'warning' 66 | }).finally(() => { 67 | store.dispatch('user/resetToken').then(() => { 68 | location.reload() 69 | }) 70 | }) 71 | } else { 72 | Message({ 73 | message: error.response.data.message || error.message, 74 | type: 'error', 75 | duration: 5 * 1000 76 | }) 77 | return Promise.reject(error) 78 | } 79 | } 80 | ) 81 | 82 | export default service 83 | -------------------------------------------------------------------------------- /src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {string} path 4 | * @returns {Boolean} 5 | */ 6 | export function isExternal(path) { 7 | return /^(https?:|mailto:|tel:)/.test(path) 8 | } 9 | 10 | /** 11 | * @param {string} url 12 | * @returns {Boolean} 13 | */ 14 | export function validURL(url) { 15 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 16 | return reg.test(url) 17 | } 18 | 19 | /** 20 | * @param {string} str 21 | * @returns {Boolean} 22 | */ 23 | export function validLowerCase(str) { 24 | const reg = /^[a-z]+$/ 25 | return reg.test(str) 26 | } 27 | 28 | /** 29 | * @param {string} str 30 | * @returns {Boolean} 31 | */ 32 | export function validUpperCase(str) { 33 | const reg = /^[A-Z]+$/ 34 | return reg.test(str) 35 | } 36 | 37 | /** 38 | * @param {string} str 39 | * @returns {Boolean} 40 | */ 41 | export function validAlphabets(str) { 42 | const reg = /^[A-Za-z]+$/ 43 | return reg.test(str) 44 | } 45 | 46 | /** 47 | * @param {string} email 48 | * @returns {Boolean} 49 | */ 50 | export function validEmail(email) { 51 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 52 | return reg.test(email) 53 | } 54 | 55 | /** 56 | * @param {string} str 57 | * @returns {Boolean} 58 | */ 59 | export function isString(str) { 60 | if (typeof str === 'string' || str instanceof String) { 61 | return true 62 | } 63 | return false 64 | } 65 | 66 | /** 67 | * @param {Array} arg 68 | * @returns {Boolean} 69 | */ 70 | export function isArray(arg) { 71 | if (typeof Array.isArray === 'undefined') { 72 | return Object.prototype.toString.call(arg) === '[object Array]' 73 | } 74 | return Array.isArray(arg) 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/websocket.js: -------------------------------------------------------------------------------- 1 | import { getToken } from '@/utils/auth' 2 | import { Message } from 'element-ui' 3 | const WSS_URL = `wss://go-vue.bigfool.cn/chat?token=` 4 | let Socket = '' 5 | 6 | /** 建立连接 */ 7 | export function createSocket() { 8 | if (!Socket || (Socket && Socket.readyState === WebSocket.CLOSED)) { 9 | Socket = new WebSocket(WSS_URL + getToken()) 10 | Socket.onopen = wsOpen 11 | Socket.onmessage = wsMessage 12 | Socket.onerror = wsError 13 | Socket.onclose = wsClose 14 | } 15 | } 16 | 17 | /** 打开WS */ 18 | export function wsOpen() { 19 | console.log('ws已连接') 20 | } 21 | 22 | /** 连接失败重连 */ 23 | export function wsError() { 24 | Message.error('websocket连接失败') 25 | Socket = '' 26 | } 27 | 28 | /** WS数据接收统一处理 */ 29 | export function wsMessage(event) { 30 | window.dispatchEvent(new CustomEvent('chatMessage', { 31 | detail: event.data 32 | })) 33 | } 34 | 35 | /** 发送数据 36 | * @param data 37 | */ 38 | export function sendWebSocketMsg(data) { 39 | if (Socket !== null && Socket.readyState === WebSocket.CLOSED) { 40 | Socket = '' 41 | createSocket() // 重连 42 | } else if (Socket.readyState === WebSocket.OPEN) { 43 | Socket.send(JSON.stringify(data)) 44 | } else if (Socket.readyState === WebSocket.CONNECTING) { 45 | setTimeout(() => { 46 | Socket.send(JSON.stringify(data)) 47 | }, 5000) 48 | } 49 | } 50 | 51 | /** 关闭WS */ 52 | export function wsClose() { 53 | if (Socket) { 54 | Socket.close() 55 | } 56 | console.log('ws已关闭') 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/views/dashboard/components/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null 8 | } 9 | }, 10 | mounted() { 11 | this.$_resizeHandler = debounce(() => { 12 | if (this.chart) { 13 | this.chart.resize() 14 | } 15 | }, 100) 16 | this.$_initResizeEvent() 17 | this.$_initSidebarResizeEvent() 18 | }, 19 | beforeDestroy() { 20 | this.$_destroyResizeEvent() 21 | this.$_destroySidebarResizeEvent() 22 | }, 23 | // to fixed bug when cached by keep-alive 24 | activated() { 25 | this.$_initResizeEvent() 26 | this.$_initSidebarResizeEvent() 27 | }, 28 | deactivated() { 29 | this.$_destroyResizeEvent() 30 | this.$_destroySidebarResizeEvent() 31 | }, 32 | methods: { 33 | // use $_ for mixins properties 34 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 35 | $_initResizeEvent() { 36 | window.addEventListener('resize', this.$_resizeHandler) 37 | }, 38 | $_destroyResizeEvent() { 39 | window.removeEventListener('resize', this.$_resizeHandler) 40 | }, 41 | $_sidebarResizeHandler(e) { 42 | if (e.propertyName === 'width') { 43 | this.$_resizeHandler() 44 | } 45 | }, 46 | $_initSidebarResizeEvent() { 47 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 48 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 49 | }, 50 | $_destroySidebarResizeEvent() { 51 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /src/views/demo/demo-1/demo-1-1/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/demo/demo-1/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/demo/demo-2/demo-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/demo/demo-2/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/error-page/403.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 59 | 60 | 99 | -------------------------------------------------------------------------------- /src/views/login/auth-redirect.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------