├── frontend ├── static │ └── .gitkeep ├── .eslintignore ├── favicon.ico ├── build │ ├── logo.png │ ├── vue-loader.conf.js │ ├── build.js │ ├── check-versions.js │ ├── webpack.dev.conf.js │ ├── webpack.base.conf.js │ ├── utils.js │ └── webpack.prod.conf.js ├── src │ ├── styles │ │ ├── variables.scss │ │ ├── mixin.scss │ │ ├── element-ui.scss │ │ ├── transition.scss │ │ ├── index.scss │ │ └── sidebar.scss │ ├── assets │ │ ├── face.gif │ │ └── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ ├── views │ │ ├── nested │ │ │ ├── menu2 │ │ │ │ └── index.vue │ │ │ └── menu1 │ │ │ │ ├── menu1-3 │ │ │ │ └── index.vue │ │ │ │ ├── index.vue │ │ │ │ ├── menu1-2 │ │ │ │ ├── menu1-2-1 │ │ │ │ │ └── index.vue │ │ │ │ ├── menu1-2-2 │ │ │ │ │ └── index.vue │ │ │ │ └── index.vue │ │ │ │ └── menu1-1 │ │ │ │ └── index.vue │ │ ├── layout │ │ │ ├── components │ │ │ │ ├── index.js │ │ │ │ ├── AppMain.vue │ │ │ │ ├── Sidebar │ │ │ │ │ ├── index.vue │ │ │ │ │ └── SidebarItem.vue │ │ │ │ └── Navbar.vue │ │ │ ├── mixin │ │ │ │ └── ResizeHandler.js │ │ │ └── Layout.vue │ │ ├── dashboard │ │ │ └── index.vue │ │ ├── table │ │ │ └── index.vue │ │ ├── tree │ │ │ └── index.vue │ │ ├── form │ │ │ ├── createuser.vue │ │ │ └── index.vue │ │ ├── login │ │ │ └── index.vue │ │ └── 404.vue │ ├── api │ │ ├── table.js │ │ ├── createuser.js │ │ └── login.js │ ├── App.vue │ ├── directive │ │ └── waves │ │ │ ├── index.js │ │ │ ├── waves.css │ │ │ └── waves.js │ ├── icons │ │ ├── index.js │ │ └── svg │ │ │ ├── table.svg │ │ │ ├── user.svg │ │ │ ├── example.svg │ │ │ ├── nested.svg │ │ │ ├── password.svg │ │ │ ├── form.svg │ │ │ ├── eye.svg │ │ │ └── tree.svg │ ├── utils │ │ ├── auth.js │ │ ├── validate.js │ │ ├── index.js │ │ ├── scrollTo.js │ │ ├── request.js │ │ └── drag.js │ ├── store │ │ ├── index.js │ │ ├── getters.js │ │ └── modules │ │ │ ├── app.js │ │ │ └── user.js │ ├── components │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ └── Pagination │ │ │ └── index.vue │ ├── permission.js │ ├── main.js │ └── router │ │ └── index.js ├── .travis.yml ├── config │ ├── prod.env.js │ ├── dev.env.js │ └── index.js ├── .gitignore ├── .babelrc ├── .postcssrc.js ├── .editorconfig ├── index.html ├── LICENSE ├── package.json └── .eslintrc.js ├── backend ├── app │ ├── config │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── dev.py │ │ └── prod.py │ ├── api │ │ ├── user │ │ │ ├── __init__.py │ │ │ ├── .DS_Store │ │ │ └── user.py │ │ └── __init__.py │ ├── utils │ │ ├── __init__.py │ │ ├── token.py │ │ └── log.py │ ├── .DS_Store │ ├── models │ │ ├── __init__.py │ │ ├── base.py │ │ ├── role.py │ │ └── user.py │ ├── marshalling │ │ ├── __init__.py │ │ ├── general.py │ │ ├── role.py │ │ └── user.py │ └── __init__.py ├── .gitignore ├── .DS_Store ├── app.py ├── .idea │ ├── vcs.xml │ ├── modules.xml │ ├── misc.xml │ ├── restplus.iml │ ├── inspectionProfiles │ │ └── Project_Default.xml │ └── workspace.xml ├── manage.py ├── requirements.txt └── run.py ├── img ├── backend.jpg ├── backend2.jpg ├── frontend.jpg └── frontend2.jpg ├── .idea ├── vcs.xml ├── misc.xml ├── modules.xml ├── flask-vue-template.iml ├── inspectionProfiles │ └── Project_Default.xml └── workspace.xml └── README.md /frontend/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .settings import APP_ENV -------------------------------------------------------------------------------- /backend/app/api/user/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import UserView,RoleView -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | src/assets 4 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__ 2 | migrations/ 3 | *.pyc 4 | venv/ 5 | logs/ 6 | -------------------------------------------------------------------------------- /backend/app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .token import token_required 2 | from .log import log 3 | -------------------------------------------------------------------------------- /backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/backend/.DS_Store -------------------------------------------------------------------------------- /img/backend.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/img/backend.jpg -------------------------------------------------------------------------------- /img/backend2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/img/backend2.jpg -------------------------------------------------------------------------------- /img/frontend.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/img/frontend.jpg -------------------------------------------------------------------------------- /img/frontend2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/img/frontend2.jpg -------------------------------------------------------------------------------- /backend/app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/backend/app/.DS_Store -------------------------------------------------------------------------------- /frontend/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/frontend/favicon.ico -------------------------------------------------------------------------------- /frontend/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/frontend/build/logo.png -------------------------------------------------------------------------------- /frontend/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | //sidebar 2 | $menuBg:#304156; 3 | $subMenuBg:#1f2d3d; 4 | $menuHover:#001528; 5 | -------------------------------------------------------------------------------- /frontend/src/assets/face.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/frontend/src/assets/face.gif -------------------------------------------------------------------------------- /backend/app/api/user/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/backend/app/api/user/.DS_Store -------------------------------------------------------------------------------- /frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: stable 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/frontend/src/assets/404_images/404.png -------------------------------------------------------------------------------- /frontend/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"', 4 | BASE_API: '"http://127.0.0.1:5000/v1"', 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishforks/flask-vue-template/HEAD/frontend/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /backend/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | 5 | from .user import User 6 | from .role import Role 7 | -------------------------------------------------------------------------------- /backend/app.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | 3 | app = create_app() 4 | 5 | if __name__ == '__main__': 6 | app.run(host='0.0.0.0', port='5000', debug=True) 7 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /backend/app/config/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from .dev import DevelopmentConfig 4 | from .prod import ProductionConfig 5 | 6 | 7 | APP_ENV = DevelopmentConfig 8 | -------------------------------------------------------------------------------- /frontend/src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList() { 4 | return request({ 5 | url: '/users/', 6 | method: 'get' 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/views/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-3/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /backend/app/marshalling/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_marshmallow import Marshmallow 2 | ma = Marshmallow() 3 | from .general import gma 4 | from .role import role_schema,roles_schema 5 | from .user import user_schema,users_schema 6 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"', 7 | BASE_API: '"http://127.0.0.1:5000/v1"', 8 | }) 9 | -------------------------------------------------------------------------------- /backend/app/marshalling/general.py: -------------------------------------------------------------------------------- 1 | from . import ma 2 | from flask_marshmallow import base_fields 3 | # 通用序列化{"code":xxx,"data":xxx} 4 | class GeneralSchema(ma.Schema): 5 | code = base_fields.Integer() 6 | data = ma.Dict() 7 | 8 | gma = GeneralSchema() -------------------------------------------------------------------------------- /backend/app/models/base.py: -------------------------------------------------------------------------------- 1 | from . import db 2 | from datetime import datetime 3 | 4 | class Base(db.Model): 5 | __abstract__ = True 6 | # id = db.Column(db.Integer, primary_key=True) 7 | # create_time = db.Column(db.DateTime,default=datetime.now) 8 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | 9 | # Editor directories and files 10 | .idea 11 | .vscode 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | -------------------------------------------------------------------------------- /backend/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins":["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /backend/app/marshalling/role.py: -------------------------------------------------------------------------------- 1 | from . import ma 2 | from app.models import Role 3 | # 序列化角色表 4 | class RoleSchema(ma.ModelSchema): 5 | class Meta: 6 | # fields = ('name',) 7 | model = Role 8 | 9 | role_schema = RoleSchema() 10 | roles_schema = RoleSchema(many=True) 11 | 12 | 13 | -------------------------------------------------------------------------------- /backend/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/api/createuser.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function createuser(username, password, role) { 4 | return request({ 5 | url: '/user/createuser/', 6 | method: 'post', 7 | data: { 8 | username, 9 | password, 10 | role 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /backend/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://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 | -------------------------------------------------------------------------------- /frontend/src/directive/waves/index.js: -------------------------------------------------------------------------------- 1 | import waves from './waves' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('waves', waves) 5 | } 6 | 7 | if (window.Vue) { 8 | window.waves = waves 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | waves.install = install 13 | export default waves 14 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Template 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg组件 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const requireAll = requireContext => requireContext.keys().map(requireContext) 8 | const req = require.context('./svg', false, /\.svg$/) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import app from './modules/app' 4 | import user from './modules/user' 5 | import getters from './getters' 6 | 7 | Vue.use(Vuex) 8 | 9 | const store = new Vuex.Store({ 10 | modules: { 11 | app, 12 | user 13 | }, 14 | getters 15 | }) 16 | 17 | export default store 18 | -------------------------------------------------------------------------------- /frontend/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name, 7 | roles: state => state.user.roles, 8 | userinfo: state => state.user.userinfo 9 | } 10 | export default getters 11 | -------------------------------------------------------------------------------- /backend/app/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import Api 2 | 3 | authorizations = { 4 | 'apikey': { 5 | 'type': 'apiKey', 6 | 'in': 'header', 7 | 'name': 'X-Token' 8 | } 9 | } 10 | 11 | api = Api(version='1.0', title='OPS API', description='OPS API', authorizations=authorizations) 12 | api.namespaces.pop(0) 13 | ns = api.namespace('v1', description='这是自定义名称空间') 14 | from .user import UserView 15 | -------------------------------------------------------------------------------- /backend/app/models/role.py: -------------------------------------------------------------------------------- 1 | from . import db 2 | from .base import Base 3 | from datetime import datetime 4 | 5 | class Role(Base): 6 | __tablename__ = 'roles' 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(80), unique=True) 9 | users = db.relationship('User',backref='role',lazy='dynamic') 10 | create_time = db.Column(db.DateTime, default=datetime.now) 11 | 12 | def __repr__(self): 13 | return '' % self.name -------------------------------------------------------------------------------- /.idea/flask-vue-template.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /frontend/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 | &::-webkit-scrollbar { 14 | width: 6px; 15 | } 16 | &::-webkit-scrollbar-thumb { 17 | background: #99a9bf; 18 | border-radius: 20px; 19 | } 20 | } 21 | 22 | @mixin relative { 23 | position: relative; 24 | width: 100%; 25 | height: 100%; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /backend/app/marshalling/user.py: -------------------------------------------------------------------------------- 1 | from . import ma 2 | from app.models import User 3 | from flask_marshmallow import base_fields 4 | 5 | # 序列化用户信息 6 | class UserInfoSchema(ma.Schema): 7 | code = base_fields.Integer() 8 | data = ma.Dict() 9 | user_info_schema = UserInfoSchema() 10 | 11 | # 序列化用户表 12 | class UserSchema(ma.ModelSchema): 13 | class Meta: 14 | # fields = ('username','password') 15 | model = User 16 | role = ma.HyperlinkRelated('role') 17 | user_schema = UserSchema() 18 | users_schema = UserSchema(many=True) -------------------------------------------------------------------------------- /frontend/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(username, password) { 4 | return request({ 5 | url: '/user/login', 6 | method: 'post', 7 | data: { 8 | username, 9 | password 10 | } 11 | }) 12 | } 13 | 14 | export function getInfo(token) { 15 | return request({ 16 | url: '/user/info', 17 | method: 'get', 18 | params: { token } 19 | }) 20 | } 21 | 22 | export function logout() { 23 | return request({ 24 | url: '/user/logout', 25 | method: 'post' 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | //to reset element-ui default css 2 | .el-upload { 3 | input[type="file"] { 4 | display: none !important; 5 | } 6 | } 7 | 8 | .el-upload__input { 9 | display: none; 10 | } 11 | 12 | //暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461 13 | .el-dialog { 14 | transform: none; 15 | left: 0; 16 | position: relative; 17 | margin: 0 auto; 18 | } 19 | 20 | //element ui upload 21 | .upload-container { 22 | .el-upload { 23 | width: 100%; 24 | .el-upload-dragger { 25 | width: 100%; 26 | height: 200px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | //globl 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*/ 15 | .breadcrumb-enter-active, 16 | .breadcrumb-leave-active { 17 | transition: all .5s; 18 | } 19 | 20 | .breadcrumb-enter, 21 | .breadcrumb-leave-active { 22 | opacity: 0; 23 | transform: translateX(20px); 24 | } 25 | 26 | .breadcrumb-move { 27 | transition: all .5s; 28 | } 29 | 30 | .breadcrumb-leave-active { 31 | position: absolute; 32 | } 33 | -------------------------------------------------------------------------------- /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from .models import db 3 | from .api import api 4 | from .marshalling import ma 5 | from flask_cors import CORS 6 | from app.utils import log 7 | 8 | cors = CORS() 9 | 10 | def register_plugin(app): 11 | api.init_app(app) 12 | db.init_app(app) 13 | ma.init_app(app) 14 | cors.init_app(app) 15 | log.init_app(app) 16 | 17 | with app.app_context(): 18 | # db.drop_all() 19 | db.create_all() 20 | 21 | 22 | def create_app(): 23 | app = Flask(__name__) 24 | app.config.from_object('app.config.APP_ENV') 25 | register_plugin(app) 26 | return app -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | from app.models import db 2 | from app import create_app 3 | from app.models.base import Base 4 | from app.models.role import Role 5 | from app.models.user import User 6 | from flask_script import Manager, Shell 7 | from flask_migrate import Migrate, MigrateCommand 8 | 9 | 10 | ''' 11 | python3 manage.py db init 12 | python3 manage.py db migrate -m "initial migration" 13 | python3 manage.py db upgrade 14 | ''' 15 | 16 | 17 | app = create_app() 18 | manager = Manager(app) 19 | migrate = Migrate(app, db) 20 | 21 | manager.add_command('db', MigrateCommand) 22 | 23 | if __name__ == '__main__': 24 | manager.run() 25 | -------------------------------------------------------------------------------- /frontend/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/views/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==3.0.2 2 | apispec==0.39.0 3 | asn1crypto==0.24.0 4 | cffi==1.11.5 5 | click==6.7 6 | cryptography==2.3 7 | Flask==1.0.2 8 | Flask-Cors==3.0.6 9 | Flask-HTTPAuth==3.2.4 10 | flask-marshmallow==0.9.0 11 | flask-restplus==0.11.0 12 | flask-restplus-marshmallow==0.2.20 13 | Flask-SQLAlchemy==2.3.2 14 | idna==2.7 15 | itsdangerous==0.24 16 | Jinja2==2.10 17 | jsonschema==2.6.0 18 | MarkupSafe==1.0 19 | marshmallow==2.15.4 20 | marshmallow-sqlalchemy==0.14.0 21 | passlib==1.7.1 22 | pycparser==2.18 23 | PyMySQL==0.9.2 24 | pytz==2018.5 25 | PyYAML==3.13 26 | six==1.11.0 27 | SQLAlchemy==1.2.10 28 | webargs==4.0.0 29 | Werkzeug==0.14.1 30 | certifi==2018.8.24 31 | chardet==3.0.4 32 | idna==2.7 33 | multi-key-dict==2.0.3 34 | pbr==4.2.0 35 | python-gitlab==1.6.0 36 | python-jenkins==1.2.1 37 | requests==2.19.1 38 | six==1.11.0 39 | urllib3==1.23 -------------------------------------------------------------------------------- /frontend/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 | 43 | -------------------------------------------------------------------------------- /backend/app/config/dev.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | # Dev config file 5 | basedir = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | class DbConf: 8 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 9 | # SQLALCHEMY_TRACH_MODIFICATIONS = False 10 | SQLALCHEMY_TRACK_MODIFICATIONS = True 11 | SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') 12 | # 安全配置 13 | CSRF_ENABLED = True 14 | SECRET_KEY = 'jklklsadhfjkhwbii9/sdf\sdf' 15 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@127.0.0.1:3306/PMS?charset=utf8mb4' 16 | # SQLALCHEMY_ECHO = True 17 | 18 | 19 | class LogConf: 20 | LOGPATH = "logs" 21 | LOGNAME = time.strftime('%Y-%m-%d', time.localtime(time.time())) 22 | LOGFORMAT = "%(asctime)s - %(levelname)s - %(filename)s - %(lineno)s - %(message)s" 23 | LOGLEVEL = "INFO" 24 | 25 | 26 | class DevelopmentConfig(DbConf,LogConf): 27 | pass -------------------------------------------------------------------------------- /backend/.idea/restplus.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 24 | -------------------------------------------------------------------------------- /frontend/src/directive/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /backend/app/config/prod.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | # Prod config file 5 | basedir = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | class DbConf: 8 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 9 | # SQLALCHEMY_TRACH_MODIFICATIONS = False 10 | SQLALCHEMY_TRACK_MODIFICATIONS = True 11 | SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') 12 | # 安全配置 13 | CSRF_ENABLED = True 14 | SECRET_KEY = 'jklklsadhfjkhwbii9/sdf\sdf' 15 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://devops:devopsSunmi666@104.250.34.119:3306/athena_dev?charset=utf8' 16 | # SQLALCHEMY_ECHO = True 17 | 18 | 19 | class LogConf: 20 | LOGPATH = "logs" 21 | LOGNAME = time.strftime('%Y-%m-%d', time.localtime(time.time())) 22 | LOGFORMAT = "%(asctime)s - %(levelname)s - %(filename)s - %(lineno)s - %(message)s" 23 | LOGLEVEL = "INFO" 24 | 25 | 26 | class ProductionConfig(DbConf,LogConf): 27 | pass -------------------------------------------------------------------------------- /backend/app/utils/token.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask import request 3 | from itsdangerous import SignatureExpired, BadSignature 4 | from app.models import User 5 | from flask import jsonify 6 | 7 | def token_required(f): 8 | ''' 9 | 验证前端发来请求头中携带的token 10 | ''' 11 | @wraps(f) 12 | def decorated(*args,**kwargs): 13 | token = None 14 | if 'X-Token' in request.headers: 15 | token = request.headers['X-Token'] 16 | try: 17 | User.verify_auth_token(token) 18 | except SignatureExpired as e: 19 | return jsonify({"code":50014,"message": "token过期"}) # valid token, but expired 20 | except BadSignature as e: 21 | return jsonify({"code": 50008, "message": "无效token"}) # invalid token 22 | except Exception as e: 23 | return jsonify({"code": 50012, "message": "其他客户端登录"}) 24 | return f(*args,**kwargs) 25 | return decorated 26 | -------------------------------------------------------------------------------- /frontend/src/views/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | -------------------------------------------------------------------------------- /backend/app/utils/log.py: -------------------------------------------------------------------------------- 1 | import logging,os 2 | from app.config import APP_ENV 3 | 4 | class Log: 5 | ''' 6 | 日志工具类,使用示例: 7 | from flask import current_app 8 | current_app.logger.info('yyyy') 9 | ''' 10 | def __init__(self): 11 | # 创建日志目录 12 | log_path = APP_ENV.LOGPATH 13 | log_name = APP_ENV.LOGNAME + '.log' 14 | log_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) + os.sep + log_path 15 | if not os.path.exists(log_path): 16 | os.mkdir(log_path) 17 | # 日志文件路径 18 | log_file = log_path + os.sep + log_name 19 | # 日志处理器 20 | handler = logging.FileHandler(log_file, encoding='UTF-8') 21 | # 日志格式 22 | logging_format = logging.Formatter(APP_ENV.LOGFORMAT) 23 | handler.setFormatter(logging_format) 24 | self.handler = handler 25 | 26 | def init_app(self,app): 27 | # 通过log.init_app(app)加载到Flask实例中 28 | app.logger.setLevel(APP_ENV.LOGLEVEL) 29 | app.logger.addHandler(self.handler) 30 | 31 | 32 | log = Log() 33 | -------------------------------------------------------------------------------- /frontend/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jiachenpan on 16/11/18. 3 | */ 4 | 5 | export function isvalidUsername(str) { 6 | // const valid_map = ['admin', 'editor', 'sunmi', 'ryan', 'test'] 7 | // return valid_map.indexOf(str.trim()) >= 0 8 | return true 9 | } 10 | 11 | /* 合法uri*/ 12 | export function validateURL(textval) { 13 | const urlregex = /^(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.,?'\\+&%$#=~_-]+))*$/ 14 | return urlregex.test(textval) 15 | } 16 | 17 | /* 小写字母*/ 18 | export function validateLowerCase(str) { 19 | const reg = /^[a-z]+$/ 20 | return reg.test(str) 21 | } 22 | 23 | /* 大写字母*/ 24 | export function validateUpperCase(str) { 25 | const reg = /^[A-Z]+$/ 26 | return reg.test(str) 27 | } 28 | 29 | /* 大小写字母*/ 30 | export function validatAlphabets(str) { 31 | const reg = /^[A-Za-z]+$/ 32 | return reg.test(str) 33 | } 34 | 35 | -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 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 | -------------------------------------------------------------------------------- /frontend/src/views/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 1024 5 | const RATIO = 3 6 | 7 | export default { 8 | watch: { 9 | $route(route) { 10 | if (this.device === 'mobile' && this.sidebar.opened) { 11 | store.dispatch('CloseSideBar', { withoutAnimation: false }) 12 | } 13 | } 14 | }, 15 | beforeMount() { 16 | window.addEventListener('resize', this.resizeHandler) 17 | }, 18 | mounted() { 19 | const isMobile = this.isMobile() 20 | if (isMobile) { 21 | store.dispatch('ToggleDevice', 'mobile') 22 | store.dispatch('CloseSideBar', { withoutAnimation: true }) 23 | } 24 | }, 25 | methods: { 26 | isMobile() { 27 | const rect = body.getBoundingClientRect() 28 | return rect.width - RATIO < WIDTH 29 | }, 30 | resizeHandler() { 31 | if (!document.hidden) { 32 | const isMobile = this.isMobile() 33 | store.dispatch('ToggleDevice', isMobile ? 'mobile' : 'desktop') 34 | 35 | if (isMobile) { 36 | store.dispatch('CloseSideBar', { withoutAnimation: true }) 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const app = { 4 | state: { 5 | sidebar: { 6 | opened: !+Cookies.get('sidebarStatus'), 7 | withoutAnimation: false 8 | }, 9 | device: 'desktop' 10 | }, 11 | mutations: { 12 | TOGGLE_SIDEBAR: state => { 13 | if (state.sidebar.opened) { 14 | Cookies.set('sidebarStatus', 1) 15 | } else { 16 | Cookies.set('sidebarStatus', 0) 17 | } 18 | state.sidebar.opened = !state.sidebar.opened 19 | state.sidebar.withoutAnimation = false 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 1) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | }, 30 | actions: { 31 | ToggleSideBar: ({ commit }) => { 32 | commit('TOGGLE_SIDEBAR') 33 | }, 34 | CloseSideBar({ commit }, { withoutAnimation }) { 35 | commit('CLOSE_SIDEBAR', withoutAnimation) 36 | }, 37 | ToggleDevice({ commit }, device) { 38 | commit('TOGGLE_DEVICE', device) 39 | } 40 | } 41 | } 42 | 43 | export default app 44 | -------------------------------------------------------------------------------- /frontend/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import NProgress from 'nprogress' // Progress 进度条 4 | import 'nprogress/nprogress.css'// Progress 进度条样式 5 | import { Message } from 'element-ui' 6 | import { getToken } from '@/utils/auth' // 验权 7 | 8 | const whiteList = ['/login'] // 不重定向白名单 9 | router.beforeEach((to, from, next) => { 10 | NProgress.start() 11 | if (getToken()) { 12 | if (to.path === '/login') { 13 | next({ path: '/' }) 14 | NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it 15 | } else { 16 | if (store.getters.roles.length === 0) { 17 | store.dispatch('GetInfo').then(() => { // 拉取用户信息 18 | next() 19 | }).catch((err) => { 20 | store.dispatch('FedLogOut').then(() => { 21 | Message.error(err || 'Verification failed, please login again') 22 | next({ path: '/' }) 23 | }) 24 | }) 25 | } else { 26 | next() 27 | } 28 | } 29 | } else { 30 | if (whiteList.indexOf(to.path) !== -1) { 31 | next() 32 | } else { 33 | next('/login') 34 | NProgress.done() 35 | } 36 | } 37 | }) 38 | 39 | router.afterEach(() => { 40 | NProgress.done() // 结束Progress 41 | }) 42 | -------------------------------------------------------------------------------- /frontend/src/views/table/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask-vue-template 2 | 基于flask和vue的前后端整合框架,开箱即用 3 | 后端使用flask-restplus开发,自带swagger,基于flask_marshmallow序列化对象,orm使用flask-sqlalchemy,已经集成基于token的用户认证,日志功能 4 | 5 | # 后端backend 6 | 7 | 使用方法: 8 | 9 | 修改app/config/settings.py指定开发环境配置文件 10 | ``` 11 | APP_ENV = DevelopmentConfig 12 | ``` 13 | 14 | 根据自己情况修改app/config/dev.py配置数据库信息,数据库提前创建 15 | ``` 16 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@127.0.0.1:3306/PMS?charset=utf8mb4' 17 | ``` 18 | 19 | 初始化数据库 20 | ``` 21 | python3 manage.py db init 22 | python3 manage.py db migrate -m "initial migration" 23 | python3 manage.py db upgrade 24 | ``` 25 | 26 | 默认监听地址:host='0.0.0.0', port='5000' ,可以在app.py中修改 27 | 28 | ![](https://github.com/fish2018/flask-vue-template/blob/master/img/backend.jpg) 29 | 30 | 在swagger页面创建用户admin/admin 31 | ![](https://github.com/fish2018/flask-vue-template/blob/master/img/backend2.jpg) 32 | 33 | # 前端frontend 34 | 35 | 使用方法: 36 | 37 | 修改开发环境配置文件config/dev.env.js ,根据自己情况配置后端API地址,默认本机5000端口 38 | ``` 39 | BASE_API: '"http://127.0.0.1:5000/v1"' 40 | ``` 41 | 42 | 启动 43 | ``` 44 | npm i 45 | npm run dev 46 | ``` 47 | 48 | 默认 http://0.0.0.0:9005 ,在config/index.js中修改 49 | ``` 50 | host: '0.0.0.0', 51 | port: 9005, 52 | ``` 53 | 54 | ![](https://github.com/fish2018/flask-vue-template/blob/master/img/frontend.jpg) 55 | 56 | 登录后 57 | 58 | ![](https://github.com/fish2018/flask-vue-template/blob/master/img/frontend2.jpg) 59 | -------------------------------------------------------------------------------- /frontend/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app{ 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a, 35 | a:focus, 36 | a:hover { 37 | cursor: pointer; 38 | color: inherit; 39 | outline: none; 40 | text-decoration: none; 41 | } 42 | 43 | div:focus{ 44 | outline: none; 45 | } 46 | 47 | a:focus, 48 | a:active { 49 | outline: none; 50 | } 51 | 52 | a, 53 | a:focus, 54 | a:hover { 55 | cursor: pointer; 56 | color: inherit; 57 | text-decoration: none; 58 | } 59 | 60 | .clearfix { 61 | &:after { 62 | visibility: hidden; 63 | display: block; 64 | font-size: 0; 65 | content: " "; 66 | clear: both; 67 | height: 0; 68 | } 69 | } 70 | 71 | //main-container全局样式 72 | .app-main{ 73 | min-height: 100% 74 | } 75 | 76 | .app-container { 77 | padding: 20px; 78 | } 79 | -------------------------------------------------------------------------------- /frontend/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | 40 | 52 | -------------------------------------------------------------------------------- /frontend/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jiachenpan on 16/11/18. 3 | */ 4 | 5 | export function parseTime(time, cFormat) { 6 | if (arguments.length === 0) { 7 | return null 8 | } 9 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 10 | let date 11 | if (typeof time === 'object') { 12 | date = time 13 | } else { 14 | if (('' + time).length === 10) time = parseInt(time) * 1000 15 | date = new Date(time) 16 | } 17 | const formatObj = { 18 | y: date.getFullYear(), 19 | m: date.getMonth() + 1, 20 | d: date.getDate(), 21 | h: date.getHours(), 22 | i: date.getMinutes(), 23 | s: date.getSeconds(), 24 | a: date.getDay() 25 | } 26 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 27 | let value = formatObj[key] 28 | if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1] 29 | if (result.length > 0 && value < 10) { 30 | value = '0' + value 31 | } 32 | return value || 0 33 | }) 34 | return time_str 35 | } 36 | 37 | export function formatTime(time, option) { 38 | time = +time * 1000 39 | const d = new Date(time) 40 | const now = Date.now() 41 | 42 | const diff = (now - d) / 1000 43 | 44 | if (diff < 30) { 45 | return '刚刚' 46 | } else if (diff < 3600) { // less 1 hour 47 | return Math.ceil(diff / 60) + '分钟前' 48 | } else if (diff < 3600 * 24) { 49 | return Math.ceil(diff / 3600) + '小时前' 50 | } else if (diff < 3600 * 24 * 2) { 51 | return '1天前' 52 | } 53 | if (option) { 54 | return parseTime(time, option) 55 | } else { 56 | return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分' 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css'// A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | import locale from 'element-ui/lib/locale/lang/en' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | 11 | import App from './App' 12 | import router from './router' 13 | import store from './store' 14 | 15 | import '@/icons' // icon 16 | import '@/permission' // permission control 17 | 18 | Vue.use(ElementUI, { locale }) 19 | Vue.config.productionTip = false 20 | 21 | Vue.directive('move', { 22 | inserted: function(a) { 23 | // 鼠标按下事件 24 | a.onmousedown = function(e) { 25 | var disX = e.clientX - a.offsetLeft 26 | var disY = e.clientY - a.offsetTop 27 | 28 | if (a.setCapture) { 29 | a.setCapture() 30 | } 31 | // 鼠标移动事件-----给文档流绑定移动事件 32 | document.onmousemove = function(e) { 33 | e.preventDefault() 34 | var L = e.clientX - disX 35 | var T = e.clientY - disY 36 | 37 | L = Math.min(Math.max(L, 0), document.documentElement.clientWidth - a.offsetWidth) 38 | T = Math.min(Math.max(T, 0), document.documentElement.clientHeight - a.offsetHeight) 39 | 40 | a.style.left = L + 'px' 41 | a.style.top = T + 'px' 42 | } 43 | // 鼠标离开事件 44 | document.onmouseup = function() { 45 | document.onmousemove = document.onmousedown = null 46 | if (a.releaseCapture) { 47 | a.releaseCapture()// 拖动后在解除事件锁定 48 | } 49 | } 50 | } 51 | } 52 | }) 53 | 54 | new Vue({ 55 | el: '#app', 56 | router, 57 | store, 58 | render: h => h(App) 59 | }) 60 | -------------------------------------------------------------------------------- /frontend/src/views/tree/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 71 | 72 | -------------------------------------------------------------------------------- /frontend/src/views/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 47 | 48 | 70 | -------------------------------------------------------------------------------- /frontend/src/directive/waves/waves.js: -------------------------------------------------------------------------------- 1 | import './waves.css' 2 | 3 | export default{ 4 | bind(el, binding) { 5 | el.addEventListener('click', e => { 6 | const customOpts = Object.assign({}, binding.value) 7 | const opts = Object.assign({ 8 | ele: el, // 波纹作用元素 9 | type: 'hit', // hit 点击位置扩散 center中心点扩展 10 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 11 | }, customOpts) 12 | const target = opts.ele 13 | if (target) { 14 | target.style.position = 'relative' 15 | target.style.overflow = 'hidden' 16 | const rect = target.getBoundingClientRect() 17 | let ripple = target.querySelector('.waves-ripple') 18 | if (!ripple) { 19 | ripple = document.createElement('span') 20 | ripple.className = 'waves-ripple' 21 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 22 | target.appendChild(ripple) 23 | } else { 24 | ripple.className = 'waves-ripple' 25 | } 26 | switch (opts.type) { 27 | case 'center': 28 | ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px' 29 | ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px' 30 | break 31 | default: 32 | ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || document.body.scrollTop) + 'px' 33 | ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || document.body.scrollLeft) + 'px' 34 | } 35 | ripple.style.backgroundColor = opts.color 36 | ripple.className = 'waves-ripple z-active' 37 | return false 38 | } 39 | }, false) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/utils/scrollTo.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 | // because it's so fucking difficult to detect the scrolling element, just move them all 16 | function move(amount) { 17 | document.documentElement.scrollTop = amount 18 | document.body.parentNode.scrollTop = amount 19 | document.body.scrollTop = amount 20 | } 21 | 22 | function position() { 23 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 24 | } 25 | 26 | export function scrollTo(to, duration, callback) { 27 | const start = position() 28 | const change = to - start 29 | const increment = 20 30 | let currentTime = 0 31 | duration = (typeof (duration) === 'undefined') ? 500 : duration 32 | var animateScroll = function() { 33 | // increment the time 34 | currentTime += increment 35 | // find the value with the quadratic in-out easing function 36 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 37 | // move the document.body 38 | move(val) 39 | // do the animation unless its over 40 | if (currentTime < duration) { 41 | requestAnimFrame(animateScroll) 42 | } else { 43 | if (callback && typeof (callback) === 'function') { 44 | // the animation is done so lets callback 45 | callback() 46 | } 47 | } 48 | } 49 | animateScroll() 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/views/form/createuser.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 68 | 69 | 71 | -------------------------------------------------------------------------------- /frontend/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 30 | 31 | 45 | -------------------------------------------------------------------------------- /backend/app/models/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from . import db 4 | from .base import Base 5 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, SignatureExpired, BadSignature 6 | from passlib.apps import custom_app_context 7 | from flask import current_app 8 | from datetime import datetime 9 | 10 | 11 | class User(Base): 12 | __tablename__ = 'users' 13 | id = db.Column(db.Integer, primary_key=True) 14 | username = db.Column(db.String(80), unique=True) 15 | password = db.Column('password',db.String(200)) 16 | role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) 17 | create_time = db.Column(db.DateTime, default=datetime.now) 18 | 19 | def __init__(self,username,password=None,role=None): 20 | self.role = role 21 | self.username = username 22 | if password == None: 23 | password = "123456" 24 | self.hash_password(password) 25 | # 密码加密 26 | def hash_password(self, password): 27 | self.password = custom_app_context.encrypt(password) 28 | 29 | # 密码解析 30 | def verify_password(self, password): 31 | return custom_app_context.verify(password, self.password) 32 | 33 | # 获取token,有效时间10min 34 | def generate_auth_token(self, expiration=600000): 35 | s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration) 36 | return s.dumps({'id': self.id}) 37 | 38 | # 解析token,确认登录的用户身份 39 | @staticmethod 40 | def verify_auth_token(token): 41 | s = Serializer(current_app.config['SECRET_KEY']) 42 | try: 43 | data = s.loads(token) 44 | except SignatureExpired as e: 45 | raise e # valid token, but expired 46 | except BadSignature as e: 47 | raise e # invalid token 48 | except Exception as e: 49 | raise e 50 | user = User.query.get(data['id']) 51 | return user 52 | 53 | def __repr__(self): 54 | return '' % self.username -------------------------------------------------------------------------------- /frontend/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Message, MessageBox } from 'element-ui' 3 | import store from '../store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // 创建axios实例 7 | const service = axios.create({ 8 | baseURL: process.env.BASE_API, // api 的 base_url 9 | timeout: 5000 // 请求超时时间 10 | }) 11 | 12 | // request拦截器 13 | service.interceptors.request.use( 14 | config => { 15 | if (store.getters.token) { 16 | config.headers['X-Token'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 17 | } 18 | return config 19 | }, 20 | error => { 21 | // Do something with request error 22 | console.log(error) // for debug 23 | Promise.reject(error) 24 | } 25 | ) 26 | 27 | // response 拦截器 28 | service.interceptors.response.use( 29 | response => { 30 | /** 31 | * code为非20000是抛错 可结合自己业务进行修改 32 | */ 33 | const res = response.data 34 | if (res.code !== 20000) { 35 | Message({ 36 | message: res.message, 37 | type: 'error', 38 | duration: 5 * 1000 39 | }) 40 | 41 | // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了; 42 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 43 | MessageBox.confirm( 44 | '你已被登出,可以取消继续留在该页面,或者重新登录', 45 | '确定登出', 46 | { 47 | confirmButtonText: '重新登录', 48 | cancelButtonText: '取消', 49 | type: 'warning' 50 | } 51 | ).then(() => { 52 | store.dispatch('FedLogOut').then(() => { 53 | location.reload() // 为了重新实例化vue-router对象 避免bug 54 | }) 55 | }).catch(e => { console.log(e) }) 56 | } 57 | // return Promise.reject('error') 58 | } else { 59 | return response.data 60 | } 61 | }, 62 | error => { 63 | console.log('err' + error) // for debug 64 | Message({ 65 | message: error.message, 66 | type: 'error', 67 | duration: 5 * 1000 68 | }) 69 | return Promise.reject(error) 70 | } 71 | ) 72 | 73 | export default service 74 | -------------------------------------------------------------------------------- /frontend/src/utils/drag.js: -------------------------------------------------------------------------------- 1 | function inserted(el, binding, vNode) { 2 | el.setAttribute('style', 'position: fixed; z-index: 9999') 3 | } 4 | 5 | function bind(el, bindding, vNode) { 6 | el.setAttribute('draggable', true) 7 | let left, top, width, height 8 | el._dragstart = function(event) { 9 | event.stopPropagation() 10 | left = event.clientX - el.offsetLeft 11 | top = event.clientY - el.offsetTop 12 | width = el.offsetWidth 13 | height = el.offsetHeight 14 | } 15 | el._checkPosition = function() { // 防止被拖出边界 16 | const width = el.offsetWidth 17 | const height = el.offsetHeight 18 | let left = Math.min(el.offsetLeft, document.body.clientWidth - width) 19 | left = Math.max(0, left) 20 | let top = Math.min(el.offsetTop, document.body.clientHeight - height) 21 | top = Math.max(0, top) 22 | el.style.left = left + 'px' 23 | el.style.top = top + 'px' 24 | el.style.width = width + 'px' 25 | el.style.height = height + 'px' 26 | } 27 | el._dragEnd = function(event) { 28 | event.stopPropagation() 29 | left = event.clientX - left 30 | top = event.clientY - top 31 | el.style.left = left + 'px' 32 | el.style.top = top + 'px' 33 | el.style.width = width + 'px' 34 | el.style.height = height + 'px' 35 | el._checkPosition() 36 | } 37 | el._documentAllowDraop = function(event) { 38 | event.preventDefault() 39 | } 40 | document.body.addEventListener('dragover', el._documentAllowDraop) 41 | el.addEventListener('dragstart', el._dragstart) 42 | el.addEventListener('dragend', el._dragEnd) 43 | window.addEventListener('resize', el._checkPosition) 44 | } 45 | 46 | function unbind(el, bindding, vNode) { 47 | document.body.removeEventListener('dragover', el._documentAllowDraop) 48 | el.removeEventListener('dragstart', el._dragstart) 49 | el.removeEventListener('dragend', el._dragEnd) 50 | window.removeEventListener('resize', el._checkPosition) 51 | delete el._documentAllowDraop 52 | delete el._dragstart 53 | delete el._dragEnd 54 | delete el._checkPosition 55 | } 56 | 57 | export default { 58 | bind, 59 | unbind, 60 | inserted 61 | } 62 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33 | -------------------------------------------------------------------------------- /backend/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 89 | 90 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/views/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 53 | 54 | 95 | 96 | -------------------------------------------------------------------------------- /backend/run.py: -------------------------------------------------------------------------------- 1 | # 示例 2 | from flask import Flask 3 | from flask_restplus import Api, Resource, fields 4 | from werkzeug.contrib.fixers import ProxyFix 5 | 6 | app = Flask(__name__) 7 | app.wsgi_app = ProxyFix(app.wsgi_app) 8 | 9 | api = Api(app, version='1.0', title='TodoMVC API', 10 | description='A simple TodoMVC API', 11 | ) 12 | 13 | # 定义命名空间 14 | ns = api.namespace('v1', description='TODO operations') 15 | 16 | todo = api.model('Todo', { 17 | 'id': fields.Integer(readOnly=True, description='The task unique identifier'), 18 | 'task': fields.String(required=True, description='The task details') 19 | }) 20 | 21 | 22 | class TodoDAO(object): 23 | def __init__(self): 24 | self.counter = 0 25 | self.todos = [] 26 | 27 | def get(self, id): 28 | for todo in self.todos: 29 | if todo['id'] == id: 30 | return todo 31 | api.abort(404, "Todo {} doesn't exist".format(id)) 32 | 33 | def create(self, data): 34 | todo = data 35 | todo['id'] = self.counter = self.counter + 1 36 | self.todos.append(todo) 37 | return todo 38 | 39 | def update(self, id, data): 40 | todo = self.get(id) 41 | todo.update(data) 42 | return todo 43 | 44 | def delete(self, id): 45 | todo = self.get(id) 46 | self.todos.remove(todo) 47 | 48 | 49 | DAO = TodoDAO() 50 | DAO.create({'task': 'Build an API'}) 51 | DAO.create({'task': '?????'}) 52 | DAO.create({'task': 'profit!'}) 53 | 54 | 55 | @ns.route('/todo') 56 | class TodoList(Resource): 57 | '''获取所有todos元素,并允许通过POST来添加新的task''' 58 | @ns.doc('list_todos') 59 | @ns.marshal_list_with(todo) 60 | def get(self): 61 | '''返回所有task''' 62 | return DAO.todos 63 | 64 | @ns.doc('create_todo') 65 | @ns.expect(todo) 66 | @ns.marshal_with(todo, code=201) 67 | def post(self): 68 | '''创建一个新的task''' 69 | return DAO.create(api.payload), 201 70 | 71 | 72 | @ns.route('/todo/') 73 | @ns.response(404, 'Todo not found') 74 | @ns.param('id', 'The task identifier') 75 | class Todo(Resource): 76 | '''获取单个todo项,并允许删除操作''' 77 | @ns.doc('get_todo') 78 | @ns.marshal_with(todo) 79 | def get(self, id): 80 | '''获取id指定的todo项''' 81 | return DAO.get(id) 82 | 83 | @ns.doc('delete_todo') 84 | @ns.response(204, 'Todo deleted') 85 | def delete(self, id): 86 | '''根据id删除对应的task''' 87 | DAO.delete(id) 88 | return '', 204 89 | 90 | @ns.expect(todo) 91 | @ns.marshal_with(todo) 92 | def put(self, id): 93 | '''更新id指定的task''' 94 | return DAO.update(id, api.payload) 95 | 96 | 97 | if __name__ == '__main__': 98 | app.run(debug=True) -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin-template", 3 | "version": "3.6.0", 4 | "license": "MIT", 5 | "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", 6 | "author": "Pan ", 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js", 11 | "build:report": "npm_config_report=true node build/build.js", 12 | "lint": "eslint --ext .js,.vue src", 13 | "test": "npm run lint" 14 | }, 15 | "dependencies": { 16 | "axios": "0.17.1", 17 | "element-ui": "2.3.4", 18 | "js-cookie": "2.2.0", 19 | "normalize.css": "7.0.0", 20 | "nprogress": "0.2.0", 21 | "stylus-loader": "^3.0.2", 22 | "vue": "2.5.10", 23 | "vue-router": "3.0.1", 24 | "vuex": "3.0.1" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "7.2.3", 28 | "babel-core": "6.26.0", 29 | "babel-eslint": "8.0.3", 30 | "babel-helper-vue-jsx-merge-props": "2.0.3", 31 | "babel-loader": "7.1.2", 32 | "babel-plugin-syntax-jsx": "6.18.0", 33 | "babel-plugin-transform-runtime": "6.23.0", 34 | "babel-plugin-transform-vue-jsx": "3.5.0", 35 | "babel-preset-env": "1.6.1", 36 | "babel-preset-stage-2": "6.24.1", 37 | "chalk": "2.3.0", 38 | "copy-webpack-plugin": "4.2.3", 39 | "css-loader": "0.28.7", 40 | "eslint": "4.13.1", 41 | "eslint-friendly-formatter": "3.0.0", 42 | "eslint-loader": "1.9.0", 43 | "eslint-plugin-html": "4.0.1", 44 | "eventsource-polyfill": "0.9.6", 45 | "extract-text-webpack-plugin": "3.0.2", 46 | "file-loader": "1.1.5", 47 | "friendly-errors-webpack-plugin": "1.6.1", 48 | "html-webpack-plugin": "2.30.1", 49 | "node-notifier": "5.1.2", 50 | "node-sass": "^4.7.2", 51 | "optimize-css-assets-webpack-plugin": "3.2.0", 52 | "ora": "1.3.0", 53 | "portfinder": "1.0.13", 54 | "postcss-import": "11.0.0", 55 | "postcss-loader": "2.0.9", 56 | "postcss-url": "7.3.0", 57 | "rimraf": "2.6.2", 58 | "sass-loader": "6.0.6", 59 | "semver": "5.4.1", 60 | "shelljs": "0.7.8", 61 | "stylus": "^0.54.5", 62 | "svg-sprite-loader": "3.5.2", 63 | "uglifyjs-webpack-plugin": "1.1.3", 64 | "url-loader": "0.6.2", 65 | "vue-loader": "13.7.2", 66 | "vue-style-loader": "3.0.3", 67 | "vue-template-compiler": "2.5.10", 68 | "webpack": "3.10.0", 69 | "webpack-bundle-analyzer": "2.9.1", 70 | "webpack-dev-server": "2.9.7", 71 | "webpack-merge": "4.1.1" 72 | }, 73 | "engines": { 74 | "node": ">= 4.0.0", 75 | "npm": ">= 3.0.0" 76 | }, 77 | "browserslist": [ 78 | "> 1%", 79 | "last 2 versions", 80 | "not ie <= 8" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/views/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 79 | -------------------------------------------------------------------------------- /frontend/src/views/form/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 79 | 80 | 85 | 86 | -------------------------------------------------------------------------------- /frontend/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const HtmlWebpackPlugin = require('html-webpack-plugin') 9 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 10 | const portfinder = require('portfinder') 11 | 12 | function resolve (dir) { 13 | return path.join(__dirname, '..', dir) 14 | } 15 | 16 | const HOST = process.env.HOST 17 | const PORT = process.env.PORT && Number(process.env.PORT) 18 | 19 | const devWebpackConfig = merge(baseWebpackConfig, { 20 | module: { 21 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 22 | }, 23 | // cheap-module-eval-source-map is faster for development 24 | devtool: config.dev.devtool, 25 | 26 | // these devServer options should be customized in /config/index.js 27 | devServer: { 28 | clientLogLevel: 'warning', 29 | historyApiFallback: true, 30 | hot: true, 31 | compress: true, 32 | host: HOST || config.dev.host, 33 | port: PORT || config.dev.port, 34 | open: config.dev.autoOpenBrowser, 35 | overlay: config.dev.errorOverlay 36 | ? { warnings: false, errors: true } 37 | : false, 38 | publicPath: config.dev.assetsPublicPath, 39 | proxy: config.dev.proxyTable, 40 | quiet: true, // necessary for FriendlyErrorsPlugin 41 | watchOptions: { 42 | poll: config.dev.poll, 43 | } 44 | }, 45 | plugins: [ 46 | new webpack.DefinePlugin({ 47 | 'process.env': require('../config/dev.env') 48 | }), 49 | new webpack.HotModuleReplacementPlugin(), 50 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 51 | new webpack.NoEmitOnErrorsPlugin(), 52 | // https://github.com/ampedandwired/html-webpack-plugin 53 | new HtmlWebpackPlugin({ 54 | filename: 'index.html', 55 | template: 'index.html', 56 | inject: true, 57 | favicon: resolve('favicon.ico'), 58 | title: 'vue-element-admin' 59 | }), 60 | ] 61 | }) 62 | 63 | module.exports = new Promise((resolve, reject) => { 64 | portfinder.basePort = process.env.PORT || config.dev.port 65 | portfinder.getPort((err, port) => { 66 | if (err) { 67 | reject(err) 68 | } else { 69 | // publish the new Port, necessary for e2e tests 70 | process.env.PORT = port 71 | // add port to devServer config 72 | devWebpackConfig.devServer.port = port 73 | 74 | // Add FriendlyErrorsPlugin 75 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 76 | compilationSuccessInfo: { 77 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 78 | }, 79 | onErrors: config.dev.notifyOnErrors 80 | ? utils.createNotifierCallback() 81 | : undefined 82 | })) 83 | 84 | resolve(devWebpackConfig) 85 | } 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /frontend/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | '@': resolve('src') 38 | } 39 | }, 40 | module: { 41 | rules: [ 42 | ...(config.dev.useEslint ? [createLintingRule()] : []), 43 | { 44 | test: /\.vue$/, 45 | loader: 'vue-loader', 46 | options: vueLoaderConfig 47 | }, 48 | { 49 | test: /\.js$/, 50 | loader: 'babel-loader', 51 | include: [resolve('src'), resolve('test') ,resolve('node_modules/webpack-dev-server/client')] 52 | }, 53 | { 54 | test: /\.svg$/, 55 | loader: 'svg-sprite-loader', 56 | include: [resolve('src/icons')], 57 | options: { 58 | symbolId: 'icon-[name]' 59 | } 60 | }, 61 | { 62 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 63 | loader: 'url-loader', 64 | exclude: [resolve('src/icons')], 65 | options: { 66 | limit: 10000, 67 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 76 | } 77 | }, 78 | { 79 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 80 | loader: 'url-loader', 81 | options: { 82 | limit: 10000, 83 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 84 | } 85 | } 86 | ] 87 | }, 88 | node: { 89 | // prevent webpack from injecting useless setImmediate polyfill because Vue 90 | // source contains it (although only uses it if it's native). 91 | setImmediate: false, 92 | // prevent webpack from injecting mocks to Node native modules 93 | // that does not make sense for the client 94 | dgram: 'empty', 95 | fs: 'empty', 96 | net: 'empty', 97 | tls: 'empty', 98 | child_process: 'empty' 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /frontend/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /frontend/src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | // 主体区域 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: 180px; 7 | position: relative; 8 | } 9 | // 侧边栏 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: 180px !important; 13 | height: 100%; 14 | position: fixed; 15 | font-size: 0px; 16 | top: 0; 17 | bottom: 0; 18 | left: 0; 19 | z-index: 1001; 20 | overflow: hidden; 21 | //reset element-ui css 22 | .horizontal-collapse-transition { 23 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 24 | } 25 | .scrollbar-wrapper { 26 | height: calc(100% + 15px); 27 | .el-scrollbar__view { 28 | height: 100%; 29 | } 30 | } 31 | .is-horizontal { 32 | display: none; 33 | } 34 | a { 35 | display: inline-block; 36 | width: 100%; 37 | overflow: hidden; 38 | } 39 | .svg-icon { 40 | margin-right: 16px; 41 | } 42 | .el-menu { 43 | border: none; 44 | height: 100%; 45 | width: 100% !important; 46 | } 47 | } 48 | .hideSidebar { 49 | .sidebar-container { 50 | width: 36px !important; 51 | } 52 | .main-container { 53 | margin-left: 36px; 54 | } 55 | .submenu-title-noDropdown { 56 | padding-left: 10px !important; 57 | position: relative; 58 | .el-tooltip { 59 | padding: 0 10px !important; 60 | } 61 | } 62 | .el-submenu { 63 | overflow: hidden; 64 | &>.el-submenu__title { 65 | padding-left: 10px !important; 66 | .el-submenu__icon-arrow { 67 | display: none; 68 | } 69 | } 70 | } 71 | .el-menu--collapse { 72 | .el-submenu { 73 | &>.el-submenu__title { 74 | &>span { 75 | height: 0; 76 | width: 0; 77 | overflow: hidden; 78 | visibility: hidden; 79 | display: inline-block; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | .sidebar-container .nest-menu .el-submenu>.el-submenu__title, 86 | .sidebar-container .el-submenu .el-menu-item { 87 | min-width: 180px !important; 88 | background-color: $subMenuBg !important; 89 | &:hover { 90 | background-color: $menuHover !important; 91 | } 92 | } 93 | .el-menu--collapse .el-menu .el-submenu { 94 | min-width: 180px !important; 95 | } 96 | 97 | //适配移动端 98 | .mobile { 99 | .main-container { 100 | margin-left: 0px; 101 | } 102 | .sidebar-container { 103 | transition: transform .28s; 104 | width: 180px !important; 105 | } 106 | &.hideSidebar { 107 | .sidebar-container { 108 | transition-duration: 0.3s; 109 | transform: translate3d(-180px, 0, 0); 110 | } 111 | } 112 | } 113 | .withoutAnimation { 114 | .main-container, 115 | .sidebar-container { 116 | transition: none; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /frontend/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login, logout, getInfo } from '@/api/login' 2 | import { getToken, setToken, removeToken } from '@/utils/auth' 3 | import { createuser } from '@/api/createuser' 4 | 5 | const user = { 6 | state: { 7 | token: getToken(), 8 | name: '', 9 | avatar: '', 10 | roles: [], 11 | userinfo: '' 12 | }, 13 | 14 | mutations: { 15 | SET_TOKEN: (state, token) => { 16 | state.token = token 17 | }, 18 | SET_NAME: (state, name) => { 19 | state.name = name 20 | }, 21 | SET_AVATAR: (state, avatar) => { 22 | state.avatar = avatar 23 | }, 24 | SET_ROLES: (state, roles) => { 25 | state.roles = roles 26 | }, 27 | CREATE_USER: (state, userinfo) => { 28 | state.userinfo = userinfo 29 | } 30 | }, 31 | 32 | actions: { 33 | // 登录 34 | Login({ commit }, userInfo) { 35 | const username = userInfo.username.trim() 36 | return new Promise((resolve, reject) => { 37 | login(username, userInfo.password).then(response => { 38 | const data = response.data 39 | setToken(data.token) 40 | commit('SET_TOKEN', data.token) 41 | resolve() 42 | }).catch(error => { 43 | reject(error) 44 | }) 45 | }) 46 | }, 47 | 48 | // 获取用户信息 49 | GetInfo({ commit, state }) { 50 | return new Promise((resolve, reject) => { 51 | getInfo(state.token).then(response => { 52 | const data = response.data 53 | if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组 54 | commit('SET_ROLES', data.roles) 55 | } else { 56 | reject('getInfo: roles must be a non-null array !') 57 | } 58 | commit('SET_NAME', data.name) 59 | commit('SET_AVATAR', data.avatar) 60 | resolve(response) 61 | }).catch(error => { 62 | reject(error) 63 | }) 64 | }) 65 | }, 66 | 67 | // 登出 68 | LogOut({ commit, state }) { 69 | return new Promise((resolve, reject) => { 70 | logout(state.token).then(() => { 71 | commit('SET_TOKEN', '') 72 | commit('SET_ROLES', []) 73 | removeToken() 74 | resolve() 75 | }).catch(error => { 76 | reject(error) 77 | }) 78 | }) 79 | }, 80 | 81 | // 前端 登出 82 | FedLogOut({ commit }) { 83 | return new Promise(resolve => { 84 | commit('SET_TOKEN', '') 85 | removeToken() 86 | resolve() 87 | }) 88 | }, 89 | 90 | // 用户注册 91 | Createuser({ commit }, userInfo) { 92 | const username = userInfo.username.trim() 93 | const password = userInfo.password.trim() 94 | const role = userInfo.role.trim() 95 | return new Promise((resolve, reject) => { 96 | createuser(username, password, role).then(response => { 97 | resolve() 98 | }).catch(error => { 99 | reject(error) 100 | }) 101 | }) 102 | } 103 | 104 | } 105 | } 106 | 107 | export default user 108 | -------------------------------------------------------------------------------- /frontend/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.2.6 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: '0.0.0.0', // can be overwritten by process.env.HOST 17 | port: 9005, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: true, 19 | errorOverlay: true, 20 | notifyOnErrors: false, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | // CSS Sourcemaps off by default because relative paths are "buggy" 44 | // with this option, according to the CSS-Loader README 45 | // (https://github.com/webpack/css-loader#sourcemaps) 46 | // In our experience, they generally work as expected, 47 | // just be aware of this issue when enabling this option. 48 | cssSourceMap: false, 49 | }, 50 | 51 | build: { 52 | // Template for index.html 53 | index: path.resolve(__dirname, '../dist/index.html'), 54 | 55 | // Paths 56 | assetsRoot: path.resolve(__dirname, '../dist'), 57 | assetsSubDirectory: 'static', 58 | 59 | /** 60 | * You can set by youself according to actual condition 61 | * You will need to set this if you plan to deploy your site under a sub path, 62 | * for example GitHub pages. If you plan to deploy your site to https://foo.github.io/bar/, 63 | * then assetsPublicPath should be set to "/bar/". 64 | * In most cases please use '/' !!! 65 | */ 66 | assetsPublicPath: '/vueAdmin-template/', // If you are deployed on the root path, please use '/' 67 | 68 | /** 69 | * Source Maps 70 | */ 71 | 72 | productionSourceMap: false, 73 | // https://webpack.js.org/configuration/devtool/#production 74 | devtool: '#source-map', 75 | 76 | // Gzip off by default as many popular static hosts such as 77 | // Surge or Netlify already gzip all static assets for you. 78 | // Before setting to `true`, make sure to: 79 | // npm install --save-dev compression-webpack-plugin 80 | productionGzip: false, 81 | productionGzipExtensions: ['js', 'css'], 82 | 83 | // Run the build command with an extra argument to 84 | // View the bundle analyzer report after build finishes: 85 | // `npm run build --report` 86 | // Set to `true` or `false` to always turn it on or off 87 | bundleAnalyzerReport: process.env.npm_config_report 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | // in development-env not use lazy-loading, because lazy-loading too many pages will cause webpack hot update too slow. so only in production use lazy-loading; 5 | // detail: https://panjiachen.github.io/vue-element-admin-site/#/lazy-loading 6 | 7 | Vue.use(Router) 8 | 9 | /* Layout */ 10 | import Layout from '../views/layout/Layout' 11 | 12 | /** 13 | * hidden: true if `hidden:true` will not show in the sidebar(default is false) 14 | * alwaysShow: true if set true, will always show the root menu, whatever its child routes length 15 | * if not set alwaysShow, only more than one route under the children 16 | * it will becomes nested mode, otherwise not show the root menu 17 | * redirect: noredirect if `redirect:noredirect` will no redirct in the breadcrumb 18 | * name:'router-name' the name is used by (must set!!!) 19 | * meta : { 20 | title: 'title' the name show in submenu and breadcrumb (recommend set) 21 | icon: 'svg-name' the icon show in the sidebar, 22 | } 23 | **/ 24 | export const constantRouterMap = [ 25 | { path: '/login', component: () => import('@/views/login/index'), hidden: true }, 26 | { path: '/404', component: () => import('@/views/404'), hidden: true }, 27 | 28 | { 29 | path: '/', 30 | component: Layout, 31 | redirect: '/dashboard', 32 | name: 'Dashboard', 33 | hidden: true, 34 | children: [{ 35 | path: 'dashboard', 36 | component: () => import('@/views/dashboard/index') 37 | }] 38 | }, 39 | 40 | { 41 | path: '/example', 42 | component: Layout, 43 | redirect: '/example/table', 44 | name: 'Example', 45 | hidden: false, 46 | meta: { title: '表格-树', icon: 'example' }, 47 | children: [ 48 | { 49 | path: 'table', 50 | name: 'Table', 51 | component: () => import('@/views/table/index'), 52 | meta: { title: '用户信息', icon: 'table' } 53 | }, 54 | { 55 | path: 'tree', 56 | name: 'Tree', 57 | component: () => import('@/views/tree/index'), 58 | meta: { title: 'Tree', icon: 'tree' } 59 | } 60 | ] 61 | }, 62 | 63 | { 64 | path: '/form', 65 | component: Layout, 66 | redirect: '/example/form', 67 | name: 'Forms', 68 | hidden: false, 69 | meta: { title: '表单', icon: 'form' }, 70 | children: [ 71 | { 72 | path: 'createuser', 73 | name: 'Form1', 74 | component: () => import('@/views/form/createuser'), 75 | meta: { title: '创建用户', icon: 'form' } 76 | }, 77 | { 78 | path: 'index', 79 | name: 'Form', 80 | component: () => import('@/views/form/index'), 81 | meta: { title: 'Form', icon: 'form' } 82 | } 83 | ] 84 | }, 85 | 86 | { 87 | path: '/nested', 88 | component: Layout, 89 | redirect: '/nested/menu1', 90 | name: 'nested', 91 | hidden: false, 92 | meta: { 93 | title: 'nested', 94 | icon: 'nested' 95 | }, 96 | children: [ 97 | { 98 | path: 'menu1', 99 | component: () => import('@/views/nested/menu1/index'), // Parent router-view 100 | name: 'menu1', 101 | meta: { title: 'menu1' }, 102 | children: [ 103 | { 104 | path: 'menu1-1', 105 | component: () => import('@/views/nested/menu1/menu1-1'), 106 | name: 'menu1-1', 107 | meta: { title: 'menu1-1' } 108 | }, 109 | { 110 | path: 'menu1-2', 111 | component: () => import('@/views/nested/menu1/menu1-2'), 112 | name: 'menu1-2', 113 | meta: { title: 'menu1-2' }, 114 | children: [ 115 | { 116 | path: 'menu1-2-1', 117 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'), 118 | name: 'menu1-2-1', 119 | meta: { title: 'menu1-2-1' } 120 | }, 121 | { 122 | path: 'menu1-2-2', 123 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'), 124 | name: 'menu1-2-2', 125 | meta: { title: 'menu1-2-2' } 126 | } 127 | ] 128 | }, 129 | { 130 | path: 'menu1-3', 131 | component: () => import('@/views/nested/menu1/menu1-3'), 132 | name: 'menu1-3', 133 | meta: { title: 'menu1-3' } 134 | } 135 | ] 136 | }, 137 | { 138 | path: 'menu2', 139 | component: () => import('@/views/nested/menu2/index'), 140 | meta: { title: 'menu2' } 141 | } 142 | ] 143 | }, 144 | 145 | { path: '*', redirect: '/404', hidden: true } 146 | ] 147 | 148 | export default new Router({ 149 | // mode: 'history', //后端支持可开 150 | scrollBehavior: () => ({ y: 0 }), 151 | routes: constantRouterMap 152 | }) 153 | 154 | -------------------------------------------------------------------------------- /backend/app/api/user/user.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import Resource,fields as filed 2 | from app.api import ns 3 | from app.models import User,Role 4 | from app.marshalling import user_schema,users_schema,role_schema,gma 5 | from flask import request,jsonify,current_app 6 | from app.utils import token_required 7 | 8 | # 给swagger用 9 | class Model(object): 10 | post_model = ns.model('填写用户信息', { 11 | 'username': filed.String, 12 | 'password': filed.String, 13 | 'role': filed.String 14 | }) 15 | login_model = ns.model('登陆信息', { 16 | 'username': filed.String, 17 | 'password': filed.String 18 | }) 19 | token_model = ns.model('Token',{ 20 | 'token': filed.String 21 | }) 22 | 23 | 24 | @ns.route('/user/createuser/',endpoint='createuser',methods=['POST']) 25 | class CreateUserView(Resource): 26 | @ns.doc(body=Model.post_model, desciption='填写用户信息') 27 | def post(self): 28 | ''' 29 | 创建用户 30 | ''' 31 | json_data = request.get_json() 32 | username = json_data['username'] 33 | password = json_data['password'] 34 | role = json_data['role'] 35 | from app.models import db 36 | if Role.query.filter_by(name=role).first(): 37 | role = Role.query.filter_by(name=role).first() 38 | else: 39 | role = Role(name=role) 40 | if not User.query.filter_by(username=username).first(): 41 | user = User(username=username, password=password, role=role) 42 | db.session.add(user) 43 | db.session.commit() 44 | data = { 45 | "code": 20000, 46 | "data":None 47 | } 48 | return gma.dump(data) 49 | 50 | @ns.route('/users/',endpoint='users',methods=['GET','OPSTIONS']) 51 | class UserView(Resource): 52 | # @ns.doc(security='apikey') 53 | # decorators = [token_required] 方法一 54 | # method_decorators = [token_required] # 方法二 55 | # @token_required 方法三 56 | def get(self): 57 | ''' 58 | 获取用户列表 59 | ''' 60 | users = User.query.all() 61 | result = users_schema.dump(users) 62 | data = { 63 | 'code': 20000, 64 | 'data': result 65 | } 66 | current_app.logger.info('获取用户列表') 67 | return gma.dump(data) 68 | 69 | @ns.route('/user/',endpoint='user',doc=False) 70 | class UserView(Resource): 71 | def get(self,id): 72 | ''' 73 | 获取用户名、密码 74 | ''' 75 | user = User.query.filter_by(id=id).first() 76 | result = user_schema.dump(user) 77 | data = { 78 | "code":20000, 79 | "data":result 80 | } 81 | return gma.dump(data) 82 | 83 | @ns.route('/role/',endpoint='role',doc=False) 84 | class RoleView(Resource): 85 | def get(self,id): 86 | ''' 87 | 根据用户id获取角色 88 | ''' 89 | role = Role.query.filter_by(id=id).first() 90 | result = role_schema.dump(role) 91 | data = { 92 | "code":20000, 93 | "data":result 94 | } 95 | return gma.dump(data) 96 | 97 | @ns.route('/user/login',endpoint='login',methods=['POST']) 98 | class LoginView(Resource): 99 | @ns.doc(body=Model.login_model, desciption='登录信息') 100 | def post(self): 101 | ''' 102 | 用户登录,获取token 103 | ''' 104 | json_data = request.get_json() 105 | username = json_data['username'] 106 | password = json_data['password'] 107 | user = User.query.filter_by(username=username).first() 108 | if user.verify_password(password): 109 | token = user.generate_auth_token() 110 | else: 111 | return '验证失败' 112 | data = { 113 | 'code':20000, 114 | 'data':{ 115 | "token":token.decode('ascii') 116 | } 117 | } 118 | current_app.logger.info('登录用户') 119 | return gma.dump(data) 120 | 121 | 122 | @ns.route('/user/info',endpoint='getinfo',methods=['GET']) 123 | class UserInfo(Resource): 124 | @ns.doc(params={'token': '根据token获取用户信息'}) 125 | # @token_required 126 | def get(self,*args,**kwargs): 127 | ''' 128 | 根据token获取用户信息 129 | ''' 130 | token = request.args.get('token') 131 | try: 132 | user = User.verify_auth_token(token) 133 | except Exception as e: 134 | return jsonify({"code": 50012, "message": "token过期"}) 135 | 136 | data = { 137 | 'code': 20000, 138 | "data":{ 139 | 'token':token, 140 | 'roles':user.role.name, 141 | 'name':user.username, 142 | 'avatar': None 143 | } 144 | } 145 | current_app.logger.info('获取用户信息') 146 | return gma.dump(data) 147 | 148 | 149 | @ns.route('/user/logout',endpoint='logout',methods=['POST']) 150 | class LogoutView(Resource): 151 | @ns.doc(body=Model.token_model, desciption='token') 152 | def post(self): 153 | ''' 154 | 用户登出 155 | ''' 156 | data = { 157 | 'code' : 20000, 158 | "data":{'token':None,"message": "用户登出"} 159 | } 160 | return gma.dump(data) 161 | -------------------------------------------------------------------------------- /frontend/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | function resolve (dir) { 15 | return path.join(__dirname, '..', dir) 16 | } 17 | 18 | const env = require('../config/prod.env') 19 | 20 | const webpackConfig = merge(baseWebpackConfig, { 21 | module: { 22 | rules: utils.styleLoaders({ 23 | sourceMap: config.build.productionSourceMap, 24 | extract: true, 25 | usePostCSS: true 26 | }) 27 | }, 28 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 29 | output: { 30 | path: config.build.assetsRoot, 31 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 32 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 33 | }, 34 | plugins: [ 35 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 36 | new webpack.DefinePlugin({ 37 | 'process.env': env 38 | }), 39 | new UglifyJsPlugin({ 40 | uglifyOptions: { 41 | compress: { 42 | warnings: false 43 | } 44 | }, 45 | sourceMap: config.build.productionSourceMap, 46 | parallel: true 47 | }), 48 | // extract css into its own file 49 | new ExtractTextPlugin({ 50 | filename: utils.assetsPath('css/[name].[contenthash].css'), 51 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 52 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 53 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 54 | allChunks: false, 55 | }), 56 | // Compress extracted CSS. We are using this plugin so that possible 57 | // duplicated CSS from different components can be deduped. 58 | new OptimizeCSSPlugin({ 59 | cssProcessorOptions: config.build.productionSourceMap 60 | ? { safe: true, map: { inline: false } } 61 | : { safe: true } 62 | }), 63 | // generate dist index.html with correct asset hash for caching. 64 | // you can customize output by editing /index.html 65 | // see https://github.com/ampedandwired/html-webpack-plugin 66 | new HtmlWebpackPlugin({ 67 | filename: config.build.index, 68 | template: 'index.html', 69 | inject: true, 70 | favicon: resolve('favicon.ico'), 71 | title: 'vue-element-admin', 72 | minify: { 73 | removeComments: true, 74 | collapseWhitespace: true, 75 | removeAttributeQuotes: true 76 | // more options: 77 | // https://github.com/kangax/html-minifier#options-quick-reference 78 | }, 79 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 80 | chunksSortMode: 'dependency' 81 | }), 82 | // keep module.id stable when vender modules does not change 83 | new webpack.HashedModuleIdsPlugin(), 84 | // enable scope hoisting 85 | new webpack.optimize.ModuleConcatenationPlugin(), 86 | // split vendor js into its own file 87 | new webpack.optimize.CommonsChunkPlugin({ 88 | name: 'vendor', 89 | minChunks (module) { 90 | // any required modules inside node_modules are extracted to vendor 91 | return ( 92 | module.resource && 93 | /\.js$/.test(module.resource) && 94 | module.resource.indexOf( 95 | path.join(__dirname, '../node_modules') 96 | ) === 0 97 | ) 98 | } 99 | }), 100 | // extract webpack runtime and module manifest to its own file in order to 101 | // prevent vendor hash from being updated whenever app bundle is updated 102 | new webpack.optimize.CommonsChunkPlugin({ 103 | name: 'manifest', 104 | minChunks: Infinity 105 | }), 106 | // This instance extracts shared chunks from code splitted chunks and bundles them 107 | // in a separate chunk, similar to the vendor chunk 108 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 109 | new webpack.optimize.CommonsChunkPlugin({ 110 | name: 'app', 111 | async: 'vendor-async', 112 | children: true, 113 | minChunks: 3 114 | }), 115 | 116 | // copy custom static assets 117 | new CopyWebpackPlugin([ 118 | { 119 | from: path.resolve(__dirname, '../static'), 120 | to: config.build.assetsSubDirectory, 121 | ignore: ['.*'] 122 | } 123 | ]) 124 | ] 125 | }) 126 | 127 | if (config.build.productionGzip) { 128 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 129 | 130 | webpackConfig.plugins.push( 131 | new CompressionWebpackPlugin({ 132 | asset: '[path].gz[query]', 133 | algorithm: 'gzip', 134 | test: new RegExp( 135 | '\\.(' + 136 | config.build.productionGzipExtensions.join('|') + 137 | ')$' 138 | ), 139 | threshold: 10240, 140 | minRatio: 0.8 141 | }) 142 | ) 143 | } 144 | 145 | if (config.build.bundleAnalyzerReport) { 146 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 147 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 148 | } 149 | 150 | module.exports = webpackConfig 151 | -------------------------------------------------------------------------------- /frontend/src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 111 | 112 | 144 | 145 | 201 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: 'eslint:recommended', 13 | // required to lint *.vue files 14 | plugins: [ 15 | 'html' 16 | ], 17 | // check if imports actually resolve 18 | 'settings': { 19 | 'import/resolver': { 20 | 'webpack': { 21 | 'config': 'build/webpack.base.conf.js' 22 | } 23 | } 24 | }, 25 | // add your custom rules here 26 | //it is base on https://github.com/vuejs/eslint-config-vue 27 | rules: { 28 | 'accessor-pairs': 2, 29 | 'arrow-spacing': [2, { 30 | 'before': true, 31 | 'after': true 32 | }], 33 | 'block-spacing': [2, 'always'], 34 | 'brace-style': [2, '1tbs', { 35 | 'allowSingleLine': true 36 | }], 37 | 'camelcase': [0, { 38 | 'properties': 'always' 39 | }], 40 | 'comma-dangle': [2, 'never'], 41 | 'comma-spacing': [2, { 42 | 'before': false, 43 | 'after': true 44 | }], 45 | 'comma-style': [2, 'last'], 46 | 'constructor-super': 2, 47 | 'curly': [2, 'multi-line'], 48 | 'dot-location': [2, 'property'], 49 | 'eol-last': 2, 50 | 'eqeqeq': [2, 'allow-null'], 51 | 'generator-star-spacing': [2, { 52 | 'before': true, 53 | 'after': true 54 | }], 55 | 'handle-callback-err': [2, '^(err|error)$'], 56 | 'indent': [2, 2, { 57 | 'SwitchCase': 1 58 | }], 59 | 'jsx-quotes': [2, 'prefer-single'], 60 | 'key-spacing': [2, { 61 | 'beforeColon': false, 62 | 'afterColon': true 63 | }], 64 | 'keyword-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'new-cap': [2, { 69 | 'newIsCap': true, 70 | 'capIsNew': false 71 | }], 72 | 'new-parens': 2, 73 | 'no-array-constructor': 2, 74 | 'no-caller': 2, 75 | 'no-console': 'off', 76 | 'no-class-assign': 2, 77 | 'no-cond-assign': 2, 78 | 'no-const-assign': 2, 79 | 'no-control-regex': 2, 80 | 'no-delete-var': 2, 81 | 'no-dupe-args': 2, 82 | 'no-dupe-class-members': 2, 83 | 'no-dupe-keys': 2, 84 | 'no-duplicate-case': 2, 85 | 'no-empty-character-class': 2, 86 | 'no-empty-pattern': 2, 87 | 'no-eval': 2, 88 | 'no-ex-assign': 2, 89 | 'no-extend-native': 2, 90 | 'no-extra-bind': 2, 91 | 'no-extra-boolean-cast': 2, 92 | 'no-extra-parens': [2, 'functions'], 93 | 'no-fallthrough': 2, 94 | 'no-floating-decimal': 2, 95 | 'no-func-assign': 2, 96 | 'no-implied-eval': 2, 97 | 'no-inner-declarations': [2, 'functions'], 98 | 'no-invalid-regexp': 2, 99 | 'no-irregular-whitespace': 2, 100 | 'no-iterator': 2, 101 | 'no-label-var': 2, 102 | 'no-labels': [2, { 103 | 'allowLoop': false, 104 | 'allowSwitch': false 105 | }], 106 | 'no-lone-blocks': 2, 107 | 'no-mixed-spaces-and-tabs': 2, 108 | 'no-multi-spaces': 2, 109 | 'no-multi-str': 2, 110 | 'no-multiple-empty-lines': [2, { 111 | 'max': 1 112 | }], 113 | 'no-native-reassign': 2, 114 | 'no-negated-in-lhs': 2, 115 | 'no-new-object': 2, 116 | 'no-new-require': 2, 117 | 'no-new-symbol': 2, 118 | 'no-new-wrappers': 2, 119 | 'no-obj-calls': 2, 120 | 'no-octal': 2, 121 | 'no-octal-escape': 2, 122 | 'no-path-concat': 2, 123 | 'no-proto': 2, 124 | 'no-redeclare': 2, 125 | 'no-regex-spaces': 2, 126 | 'no-return-assign': [2, 'except-parens'], 127 | 'no-self-assign': 2, 128 | 'no-self-compare': 2, 129 | 'no-sequences': 2, 130 | 'no-shadow-restricted-names': 2, 131 | 'no-spaced-func': 2, 132 | 'no-sparse-arrays': 2, 133 | 'no-this-before-super': 2, 134 | 'no-throw-literal': 2, 135 | 'no-trailing-spaces': 2, 136 | 'no-undef': 2, 137 | 'no-undef-init': 2, 138 | 'no-unexpected-multiline': 2, 139 | 'no-unmodified-loop-condition': 2, 140 | 'no-unneeded-ternary': [2, { 141 | 'defaultAssignment': false 142 | }], 143 | 'no-unreachable': 2, 144 | 'no-unsafe-finally': 2, 145 | 'no-unused-vars': [2, { 146 | 'vars': 'all', 147 | 'args': 'none' 148 | }], 149 | 'no-useless-call': 2, 150 | 'no-useless-computed-key': 2, 151 | 'no-useless-constructor': 2, 152 | 'no-useless-escape': 0, 153 | 'no-whitespace-before-property': 2, 154 | 'no-with': 2, 155 | 'one-var': [2, { 156 | 'initialized': 'never' 157 | }], 158 | 'operator-linebreak': [2, 'after', { 159 | 'overrides': { 160 | '?': 'before', 161 | ':': 'before' 162 | } 163 | }], 164 | 'padded-blocks': [2, 'never'], 165 | 'quotes': [2, 'single', { 166 | 'avoidEscape': true, 167 | 'allowTemplateLiterals': true 168 | }], 169 | 'semi': [2, 'never'], 170 | 'semi-spacing': [2, { 171 | 'before': false, 172 | 'after': true 173 | }], 174 | 'space-before-blocks': [2, 'always'], 175 | 'space-before-function-paren': [2, 'never'], 176 | 'space-in-parens': [2, 'never'], 177 | 'space-infix-ops': 2, 178 | 'space-unary-ops': [2, { 179 | 'words': true, 180 | 'nonwords': false 181 | }], 182 | 'spaced-comment': [2, 'always', { 183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 184 | }], 185 | 'template-curly-spacing': [2, 'never'], 186 | 'use-isnan': 2, 187 | 'valid-typeof': 2, 188 | 'wrap-iife': [2, 'any'], 189 | 'yield-star-spacing': [2, 'both'], 190 | 'yoda': [2, 'never'], 191 | 'prefer-const': 2, 192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 193 | 'object-curly-spacing': [2, 'always', { 194 | objectsInObjects: false 195 | }], 196 | 'array-bracket-spacing': [2, 'never'] 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /frontend/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 42 | 43 | 237 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | 42 | 43 | 44 | 45 | true 46 | DEFINITION_ORDER 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 |