├── vue-admin
├── .eslintignore
├── babel.config.js
├── tests
│ └── unit
│ │ ├── .eslintrc.js
│ │ ├── components
│ │ ├── Hamburger.spec.js
│ │ ├── SvgIcon.spec.js
│ │ └── Breadcrumb.spec.js
│ │ └── utils
│ │ ├── validate.spec.js
│ │ ├── parseTime.spec.js
│ │ └── formatTime.spec.js
├── public
│ ├── favicon.ico
│ └── index.html
├── .travis.yml
├── src
│ ├── assets
│ │ ├── 403_images
│ │ │ └── 403.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
│ │ ├── dashboard
│ │ │ └── index.vue
│ │ ├── tree
│ │ │ └── index.vue
│ │ ├── table
│ │ │ └── index.vue
│ │ ├── 403.vue
│ │ ├── form
│ │ │ └── index.vue
│ │ ├── login
│ │ │ └── index.vue
│ │ └── 404.vue
│ ├── layout
│ │ ├── components
│ │ │ ├── index.js
│ │ │ ├── Sidebar
│ │ │ │ ├── Item.vue
│ │ │ │ ├── Link.vue
│ │ │ │ ├── FixiOSBug.js
│ │ │ │ ├── index.vue
│ │ │ │ ├── Logo.vue
│ │ │ │ └── SidebarItem.vue
│ │ │ ├── AppMain.vue
│ │ │ └── Navbar.vue
│ │ ├── mixin
│ │ │ └── ResizeHandler.js
│ │ └── index.vue
│ ├── App.vue
│ ├── api
│ │ ├── table.js
│ │ └── user.js
│ ├── store
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── modules
│ │ │ ├── settings.js
│ │ │ ├── app.js
│ │ │ └── user.js
│ ├── icons
│ │ ├── svg
│ │ │ ├── link.svg
│ │ │ ├── user.svg
│ │ │ ├── example.svg
│ │ │ ├── table.svg
│ │ │ ├── password.svg
│ │ │ ├── nested.svg
│ │ │ ├── eye.svg
│ │ │ ├── skill.svg
│ │ │ ├── eye-open.svg
│ │ │ ├── pdf.svg
│ │ │ ├── tree.svg
│ │ │ ├── dashboard.svg
│ │ │ ├── form.svg
│ │ │ └── qq.svg
│ │ ├── index.js
│ │ └── svgo.yml
│ ├── utils
│ │ ├── get-page-title.js
│ │ ├── validate.js
│ │ ├── auth.js
│ │ ├── index.js
│ │ └── request.js
│ ├── settings.js
│ ├── styles
│ │ ├── mixin.scss
│ │ ├── variables.scss
│ │ ├── element-ui.scss
│ │ ├── transition.scss
│ │ ├── index.scss
│ │ └── sidebar.scss
│ ├── main.js
│ ├── components
│ │ ├── Hamburger
│ │ │ └── index.vue
│ │ ├── SvgIcon
│ │ │ └── index.vue
│ │ └── Breadcrumb
│ │ │ └── index.vue
│ ├── permission.js
│ └── router
│ │ └── index.js
├── .env.production
├── .env.staging
├── postcss.config.js
├── .gitignore
├── .editorconfig
├── .env.development
├── mock
│ ├── table.js
│ ├── user.js
│ ├── index.js
│ └── mock-server.js
├── jest.config.js
├── build
│ └── index.js
├── LICENSE
├── package.json
├── README.md
├── README-zh.md
├── vue.config.js
└── .eslintrc.js
├── viewModels
├── User.go
├── article.go
└── emun
│ └── articleStatus.go
├── conf
└── app.ini
├── pkg
├── util
│ └── pagination.go
├── e
│ ├── code.go
│ └── msg.go
└── setting
│ └── setting.go
├── go.mod
├── models
├── Claims.go
├── auth.go
├── models.go
├── tag.go
└── article.go
├── main.go
├── README.md
├── routers
├── api
│ ├── v1
│ │ ├── auth.go
│ │ ├── tag.go
│ │ └── article.go
│ └── v2
│ │ └── article.go
└── router.go
├── LICENSE
├── middleware
├── myjwt
│ ├── myjwt.go
│ └── gin_jwt.go
└── cors
│ └── cors.go
├── docs
└── sql
│ └── mysql.sql
└── go.sum
/vue-admin/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | src/assets
3 | public
4 | dist
5 |
--------------------------------------------------------------------------------
/vue-admin/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/vue-admin/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/vue-admin/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rockyzsu/gin-vue/master/vue-admin/public/favicon.ico
--------------------------------------------------------------------------------
/vue-admin/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: 10
3 | script: npm run test
4 | notifications:
5 | email: false
6 |
--------------------------------------------------------------------------------
/vue-admin/src/assets/403_images/403.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rockyzsu/gin-vue/master/vue-admin/src/assets/403_images/403.gif
--------------------------------------------------------------------------------
/vue-admin/src/assets/404_images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rockyzsu/gin-vue/master/vue-admin/src/assets/404_images/404.png
--------------------------------------------------------------------------------
/vue-admin/.env.production:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'production'
3 |
4 | # base api
5 | VUE_APP_BASE_API = 'http://bj939496716:8000'
6 |
7 |
--------------------------------------------------------------------------------
/vue-admin/src/assets/404_images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rockyzsu/gin-vue/master/vue-admin/src/assets/404_images/404_cloud.png
--------------------------------------------------------------------------------
/vue-admin/.env.staging:
--------------------------------------------------------------------------------
1 | NODE_ENV = production
2 |
3 | # just a flag
4 | ENV = 'staging'
5 |
6 | # base api
7 | VUE_APP_BASE_API = '/stage-api'
8 |
9 |
--------------------------------------------------------------------------------
/viewModels/User.go:
--------------------------------------------------------------------------------
1 | package viewModels
2 |
3 | type User struct {
4 | Roles []string
5 | Introduction string
6 | Avatar string
7 | Name string
8 | }
9 |
--------------------------------------------------------------------------------
/vue-admin/src/views/nested/menu2/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/vue-admin/src/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 |
--------------------------------------------------------------------------------
/vue-admin/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/vue-admin/src/views/nested/menu1/menu1-3/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/vue-admin/src/views/nested/menu1/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/vue-admin/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/vue-admin/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/viewModels/article.go:
--------------------------------------------------------------------------------
1 | package viewModels
2 |
3 | type Article struct {
4 | Id int
5 | Title string
6 | Status string
7 | Author string
8 | DisplayTime string
9 | Pageviews int
10 | }
11 |
--------------------------------------------------------------------------------
/vue-admin/src/api/table.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function getList(params) {
4 | return request({
5 | url: '/api/v1/table/list',
6 | method: 'get',
7 | params
8 | })
9 | }
10 |
--------------------------------------------------------------------------------
/vue-admin/src/views/nested/menu1/menu1-1/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/vue-admin/src/views/nested/menu1/menu1-2/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/vue-admin/postcss.config.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | 'plugins': {
5 | // to edit target browsers: use "browserslist" field in package.json
6 | 'autoprefixer': {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/vue-admin/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 | }
8 | export default getters
9 |
--------------------------------------------------------------------------------
/vue-admin/.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 | tests/**/coverage/
9 |
10 | # Editor directories and files
11 | .idea
12 | .vscode
13 | *.suo
14 | *.ntvs*
15 | *.njsproj
16 | *.sln
17 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/src/utils/get-page-title.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const title = defaultSettings.title || 'Vue Admin Template'
4 |
5 | export default function getPageTitle(pageTitle) {
6 | if (pageTitle) {
7 | return `${pageTitle} - ${title}`
8 | }
9 | return `${title}`
10 | }
11 |
--------------------------------------------------------------------------------
/vue-admin/.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 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import SvgIcon from '@/components/SvgIcon'// svg component
3 |
4 | // register globally
5 | Vue.component('svg-icon', SvgIcon)
6 |
7 | const req = require.context('./svg', false, /\.svg$/)
8 | const requireAll = requireContext => requireContext.keys().map(requireContext)
9 | requireAll(req)
10 |
--------------------------------------------------------------------------------
/conf/app.ini:
--------------------------------------------------------------------------------
1 | #debug or release
2 | RUN_MODE = debug
3 |
4 | [app]
5 | PAGE_SIZE = 10
6 | IDENTITY_KEY = idname
7 |
8 |
9 | [server]
10 | HTTP_PORT = 8000
11 | READ_TIMEOUT = 60
12 | WRITE_TIMEOUT = 60
13 |
14 | [database]
15 | TYPE = mysql
16 | USER = root
17 | PASSWORD = root
18 | #127.0.0.1:3306
19 | HOST = 192.168.129.251:3306
20 | NAME = blog
21 | TABLE_PREFIX = blog_
--------------------------------------------------------------------------------
/pkg/util/pagination.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/unknwon/com"
6 |
7 | "gin-vue/pkg/setting"
8 | )
9 |
10 | func GetPage(c *gin.Context) int {
11 | result := 0
12 | page, _ := com.StrTo(c.Query("page")).Int()
13 | if page > 0 {
14 | result = (page - 1) * setting.PageSize
15 | }
16 |
17 | return result
18 | }
19 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svgo.yml:
--------------------------------------------------------------------------------
1 | # replace default config
2 |
3 | # multipass: true
4 | # full: true
5 |
6 | plugins:
7 |
8 | # - name
9 | #
10 | # or:
11 | # - name: false
12 | # - name: true
13 | #
14 | # or:
15 | # - name:
16 | # param1: 1
17 | # param2: 2
18 |
19 | - removeAttrs:
20 | attrs:
21 | - 'fill'
22 | - 'fill-rule'
23 |
--------------------------------------------------------------------------------
/vue-admin/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | title: 'Vue Admin Template',
4 |
5 | /**
6 | * @type {boolean} true | false
7 | * @description Whether fix the header
8 | */
9 | fixedHeader: false,
10 |
11 | /**
12 | * @type {boolean} true | false
13 | * @description Whether show the logo in sidebar
14 | */
15 | sidebarLogo: false
16 | }
17 |
--------------------------------------------------------------------------------
/viewModels/emun/articleStatus.go:
--------------------------------------------------------------------------------
1 | package emun
2 |
3 | var ArticleStatus = map[int]string{
4 | -1: "error", //出现未定义状态
5 | 0: "published", //已发布
6 | 1: "draft", //编辑中
7 | 2: "deleted", //已删除
8 | }
9 |
10 | func GetArticleStatus(code int) string {
11 | msg, ok := ArticleStatus[code]
12 | if ok {
13 | return msg
14 | }
15 |
16 | return ArticleStatus[-1]
17 | }
18 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module gin-vue
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/appleboy/gin-jwt v2.5.0+incompatible
7 | github.com/appleboy/gin-jwt/v2 v2.6.4
8 | github.com/astaxie/beego v1.12.2
9 | github.com/gin-gonic/gin v1.6.3
10 | github.com/go-ini/ini v1.57.0
11 | github.com/jinzhu/gorm v1.9.15
12 | github.com/unknwon/com v1.0.1
13 | gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 // indirect
14 | )
15 |
--------------------------------------------------------------------------------
/vue-admin/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import getters from './getters'
4 | import app from './modules/app'
5 | import settings from './modules/settings'
6 | import user from './modules/user'
7 |
8 | Vue.use(Vuex)
9 |
10 | const store = new Vuex.Store({
11 | modules: {
12 | app,
13 | settings,
14 | user
15 | },
16 | getters
17 | })
18 |
19 | export default store
20 |
--------------------------------------------------------------------------------
/models/Claims.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type Claims struct {
4 | ID int `gorm:"primary_key" json:"claim_id"`
5 | AuthID int `json:"auth_id"`
6 | Type string `json:"type"`
7 | Value string `json:"value"`
8 | }
9 |
10 | func GetUserClaims(userName string) (claims []Claims) {
11 | var auth Auth
12 | db.Where("username = ?", userName).First(&auth)
13 | db.Where("auth_id = ?", auth.ID).Find(&claims)
14 | return
15 | }
16 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/e/code.go:
--------------------------------------------------------------------------------
1 | package e
2 |
3 | const (
4 | SUCCESS = 200
5 | ERROR = 500
6 | INVALID_PARAMS = 400
7 |
8 | ERROR_EXIST_TAG = 10001
9 | ERROR_NOT_EXIST_TAG = 10002
10 | ERROR_NOT_EXIST_ARTICLE = 10003
11 |
12 | ERROR_AUTH_CHECK_TOKEN_FAIL = 20001
13 | ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002
14 | ERROR_AUTH_TOKEN = 20003
15 | ERROR_AUTH = 20004
16 |
17 | PAGE_NOT_FOUND = 40001
18 | )
19 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "gin-vue/pkg/setting"
8 | "gin-vue/routers"
9 | )
10 |
11 | func main() {
12 | router := routers.InitRouter()
13 |
14 | s := &http.Server{
15 | Addr: fmt.Sprintf(":%d", setting.HTTPPort),
16 | Handler: router,
17 | ReadTimeout: setting.ReadTimeout,
18 | WriteTimeout: setting.WriteTimeout,
19 | MaxHeaderBytes: 1 << 20,
20 | }
21 |
22 | s.ListenAndServe()
23 | }
24 |
--------------------------------------------------------------------------------
/vue-admin/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * @param {string} path
7 | * @returns {Boolean}
8 | */
9 | export function isExternal(path) {
10 | return /^(https?:|mailto:|tel:)/.test(path)
11 | }
12 |
13 | /**
14 | * @param {string} str
15 | * @returns {Boolean}
16 | */
17 | export function validUsername(str) {
18 | const valid_map = ['admin', 'editor']
19 | return valid_map.indexOf(str.trim()) >= 0
20 | }
21 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gin-vue
2 |
3 | 该项目是gin+vue的前后端分离项目,使用gorm访问MySQL
4 |
5 | 使用jwt,对API接口进行权限控制。[教程](https://www.cnblogs.com/FireworksEasyCool/p/11455834.html)
6 |
7 | 在token过期后的半个小时内,用户再次操作会自动刷新token
8 |
9 | ### go后台程序运行方式
10 |
11 | 1.在MySQL中运行文件夹/docs/sql中的mysql.sql脚本
12 |
13 | 2.在文件夹/conf中修改配置文件api.ini中的数据库连接配置
14 |
15 | 3.在gin-vue目录下运行`go run main.go`
16 |
17 | ### vue运行方式请看文件夹/vue-admin中的README.md
18 |
19 | 喜欢请star
20 |
21 | 推荐一个项目([gin-vue-admin](https://github.com/Bingjian-Zhu/gin-vue-admin)),它是基于这个项目的基础上改进的,用依赖注入的方式对项目进行解耦
22 |
--------------------------------------------------------------------------------
/vue-admin/src/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix {
2 | &:after {
3 | content: "";
4 | display: table;
5 | clear: both;
6 | }
7 | }
8 |
9 | @mixin scrollBar {
10 | &::-webkit-scrollbar-track-piece {
11 | background: #d3dce6;
12 | }
13 |
14 | &::-webkit-scrollbar {
15 | width: 6px;
16 | }
17 |
18 | &::-webkit-scrollbar-thumb {
19 | background: #99a9bf;
20 | border-radius: 20px;
21 | }
22 | }
23 |
24 | @mixin relative {
25 | position: relative;
26 | width: 100%;
27 | height: 100%;
28 | }
29 |
--------------------------------------------------------------------------------
/vue-admin/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
19 |
20 |
31 |
--------------------------------------------------------------------------------
/vue-admin/src/api/user.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function login(data) {
4 | return request({
5 | url: '/login',
6 | method: 'post',
7 | data
8 | })
9 | }
10 |
11 | export function getInfo() {
12 | return request({
13 | url: '/user/info',
14 | method: 'get'
15 | })
16 | }
17 |
18 | export function refreshToken() {
19 | return request({
20 | url: '/auth/refresh_token',
21 | method: 'get'
22 | })
23 | }
24 |
25 | export function logout() {
26 | return request({
27 | url: '/user/logout',
28 | method: 'post'
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/.env.development:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'development'
3 |
4 | # base api
5 | VUE_APP_BASE_API = 'http://localhost:8000'
6 |
7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled.
9 | # It only does one thing by converting all import() to require().
10 | # This configuration can significantly increase the speed of hot updates,
11 | # when you have a large number of pages.
12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
13 |
14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true
15 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/vue-admin/mock/table.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 |
3 | const data = Mock.mock({
4 | 'items|30': [{
5 | id: '@id',
6 | title: '@sentence(10, 20)',
7 | 'status|1': ['published', 'draft', 'deleted'],
8 | author: 'name',
9 | display_time: '@datetime',
10 | pageviews: '@integer(300, 5000)'
11 | }]
12 | })
13 |
14 | export default [
15 | {
16 | url: '/table/list',
17 | type: 'get',
18 | response: config => {
19 | const items = data.items
20 | return {
21 | code: 20000,
22 | data: {
23 | total: items.length,
24 | items: items
25 | }
26 | }
27 | }
28 | }
29 | ]
30 |
--------------------------------------------------------------------------------
/vue-admin/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
4 |
5 | const state = {
6 | showSettings: showSettings,
7 | fixedHeader: fixedHeader,
8 | sidebarLogo: sidebarLogo
9 | }
10 |
11 | const mutations = {
12 | CHANGE_SETTING: (state, { key, value }) => {
13 | if (state.hasOwnProperty(key)) {
14 | state[key] = value
15 | }
16 | }
17 | }
18 |
19 | const actions = {
20 | changeSetting({ commit }, data) {
21 | commit('CHANGE_SETTING', data)
22 | }
23 | }
24 |
25 | export default {
26 | namespaced: true,
27 | state,
28 | mutations,
29 | actions
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/vue-admin/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= webpackConfig.name %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/vue-admin/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const TokenKey = 'jwt-token'
4 | const TokenExpireKey = 'token-expire'
5 |
6 | export function getToken() {
7 | return Cookies.get(TokenKey)
8 | }
9 |
10 | export function setToken(token) {
11 | return Cookies.set(TokenKey, token)
12 | }
13 |
14 | export function removeToken() {
15 | return Cookies.remove(TokenKey)
16 | }
17 |
18 | export function getTokenExpire() {
19 | return Cookies.get(TokenExpireKey)
20 | }
21 |
22 | export function setTokenExpire(tokenExpire) {
23 | return Cookies.set(TokenExpireKey, tokenExpire)
24 | }
25 |
26 | export function removeTokenExpire() {
27 | return Cookies.remove(TokenExpireKey)
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/e/msg.go:
--------------------------------------------------------------------------------
1 | package e
2 |
3 | var MsgFlags = map[int]string{
4 | SUCCESS: "ok",
5 | ERROR: "fail",
6 | INVALID_PARAMS: "请求参数错误",
7 | ERROR_EXIST_TAG: "已存在该标签名称",
8 | ERROR_NOT_EXIST_TAG: "该标签不存在",
9 | ERROR_NOT_EXIST_ARTICLE: "该文章不存在",
10 | ERROR_AUTH_CHECK_TOKEN_FAIL: "Token鉴权失败",
11 | ERROR_AUTH_CHECK_TOKEN_TIMEOUT: "Token已超时",
12 | ERROR_AUTH_TOKEN: "Token生成失败",
13 | ERROR_AUTH: "Token错误",
14 | PAGE_NOT_FOUND: "Page not found",
15 | }
16 |
17 | func GetMsg(code int) string {
18 | msg, ok := MsgFlags[code]
19 | if ok {
20 | return msg
21 | }
22 |
23 | return MsgFlags[ERROR]
24 | }
25 |
--------------------------------------------------------------------------------
/vue-admin/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // sidebar
2 | $menuText:#bfcbd9;
3 | $menuActiveText:#409EFF;
4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
5 |
6 | $menuBg:#304156;
7 | $menuHover:#263445;
8 |
9 | $subMenuBg:#1f2d3d;
10 | $subMenuHover:#001528;
11 |
12 | $sideBarWidth: 210px;
13 |
14 | // the :export directive is the magic sauce for webpack
15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
16 | :export {
17 | menuText: $menuText;
18 | menuActiveText: $menuActiveText;
19 | subMenuActiveText: $subMenuActiveText;
20 | menuBg: $menuBg;
21 | menuHover: $menuHover;
22 | subMenuBg: $subMenuBg;
23 | subMenuHover: $subMenuHover;
24 | sideBarWidth: $sideBarWidth;
25 | }
26 |
--------------------------------------------------------------------------------
/vue-admin/tests/unit/components/Hamburger.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import Hamburger from '@/components/Hamburger/index.vue'
3 | describe('Hamburger.vue', () => {
4 | it('toggle click', () => {
5 | const wrapper = shallowMount(Hamburger)
6 | const mockFn = jest.fn()
7 | wrapper.vm.$on('toggleClick', mockFn)
8 | wrapper.find('.hamburger').trigger('click')
9 | expect(mockFn).toBeCalled()
10 | })
11 | it('prop isActive', () => {
12 | const wrapper = shallowMount(Hamburger)
13 | wrapper.setProps({ isActive: true })
14 | expect(wrapper.contains('.is-active')).toBe(true)
15 | wrapper.setProps({ isActive: false })
16 | expect(wrapper.contains('.is-active')).toBe(false)
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/vue-admin/tests/unit/components/SvgIcon.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import SvgIcon from '@/components/SvgIcon/index.vue'
3 | describe('SvgIcon.vue', () => {
4 | it('iconClass', () => {
5 | const wrapper = shallowMount(SvgIcon, {
6 | propsData: {
7 | iconClass: 'test'
8 | }
9 | })
10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test')
11 | })
12 | it('className', () => {
13 | const wrapper = shallowMount(SvgIcon, {
14 | propsData: {
15 | iconClass: 'test'
16 | }
17 | })
18 | expect(wrapper.classes().length).toBe(1)
19 | wrapper.setProps({ className: 'test' })
20 | expect(wrapper.classes().includes('test')).toBe(true)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
37 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/components/Sidebar/FixiOSBug.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | device() {
4 | return this.$store.state.app.device
5 | }
6 | },
7 | mounted() {
8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug
9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135
10 | this.fixBugIniOS()
11 | },
12 | methods: {
13 | fixBugIniOS() {
14 | const $subMenu = this.$refs.subMenu
15 | if ($subMenu) {
16 | const handleMouseleave = $subMenu.handleMouseleave
17 | $subMenu.handleMouseleave = (e) => {
18 | if (this.device === 'mobile') {
19 | return
20 | }
21 | handleMouseleave(e)
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/vue-admin/tests/unit/utils/validate.spec.js:
--------------------------------------------------------------------------------
1 | import { validUsername, isExternal } from '@/utils/validate.js'
2 |
3 | describe('Utils:validate', () => {
4 | it('validUsername', () => {
5 | expect(validUsername('admin')).toBe(true)
6 | expect(validUsername('editor')).toBe(true)
7 | expect(validUsername('xxxx')).toBe(false)
8 | })
9 | it('isExternal', () => {
10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false)
13 | expect(isExternal('/dashboard')).toBe(false)
14 | expect(isExternal('./dashboard')).toBe(false)
15 | expect(isExternal('dashboard')).toBe(false)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
1 | // cover some element-ui styles
2 |
3 | .el-breadcrumb__inner,
4 | .el-breadcrumb__inner a {
5 | font-weight: 400 !important;
6 | }
7 |
8 | .el-upload {
9 | input[type="file"] {
10 | display: none !important;
11 | }
12 | }
13 |
14 | .el-upload__input {
15 | display: none;
16 | }
17 |
18 |
19 | // to fixed https://github.com/ElemeFE/element/issues/2461
20 | .el-dialog {
21 | transform: none;
22 | left: 0;
23 | position: relative;
24 | margin: 0 auto;
25 | }
26 |
27 | // refine element ui upload
28 | .upload-container {
29 | .el-upload {
30 | width: 100%;
31 |
32 | .el-upload-dragger {
33 | width: 100%;
34 | height: 200px;
35 | }
36 | }
37 | }
38 |
39 | // dropdown
40 | .el-dropdown-menu {
41 | a {
42 | display: block
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
19 |
20 |
32 |
33 |
41 |
--------------------------------------------------------------------------------
/routers/api/v1/auth.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "net/http"
5 |
6 | jwt "github.com/appleboy/gin-jwt"
7 | "github.com/gin-gonic/gin"
8 |
9 | "gin-vue/models"
10 | "gin-vue/pkg/e"
11 | "gin-vue/viewModels"
12 | )
13 |
14 | func GetUserInfo(c *gin.Context) {
15 | claims := jwt.ExtractClaims(c)
16 | userName := claims["userName"].(string)
17 | avatar := models.GetUserID(userName)
18 |
19 | code := e.SUCCESS
20 | userRoles := models.GetRoles(userName)
21 | data := viewModels.User{Roles: userRoles, Introduction: "", Avatar: avatar, Name: userName}
22 |
23 | c.JSON(http.StatusOK, gin.H{
24 | "code": code,
25 | "msg": e.GetMsg(code),
26 | "data": data,
27 | })
28 | }
29 |
30 | func Logout(c *gin.Context) {
31 | code := e.SUCCESS
32 | c.JSON(http.StatusOK, gin.H{
33 | "code": code,
34 | "msg": e.GetMsg(code),
35 | "data": "success",
36 | })
37 | }
38 |
--------------------------------------------------------------------------------
/vue-admin/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
3 | transform: {
4 | '^.+\\.vue$': 'vue-jest',
5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
6 | 'jest-transform-stub',
7 | '^.+\\.jsx?$': 'babel-jest'
8 | },
9 | moduleNameMapper: {
10 | '^@/(.*)$': '/src/$1'
11 | },
12 | snapshotSerializers: ['jest-serializer-vue'],
13 | testMatch: [
14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
15 | ],
16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
17 | coverageDirectory: '/tests/unit/coverage',
18 | // 'collectCoverage': true,
19 | 'coverageReporters': [
20 | 'lcov',
21 | 'text-summary'
22 | ],
23 | testURL: 'http://localhost/'
24 | }
25 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/src/styles/transition.scss:
--------------------------------------------------------------------------------
1 | // global transition css
2 |
3 | /* fade */
4 | .fade-enter-active,
5 | .fade-leave-active {
6 | transition: opacity 0.28s;
7 | }
8 |
9 | .fade-enter,
10 | .fade-leave-active {
11 | opacity: 0;
12 | }
13 |
14 | /* fade-transform */
15 | .fade-transform-leave-active,
16 | .fade-transform-enter-active {
17 | transition: all .5s;
18 | }
19 |
20 | .fade-transform-enter {
21 | opacity: 0;
22 | transform: translateX(-30px);
23 | }
24 |
25 | .fade-transform-leave-to {
26 | opacity: 0;
27 | transform: translateX(30px);
28 | }
29 |
30 | /* breadcrumb transition */
31 | .breadcrumb-enter-active,
32 | .breadcrumb-leave-active {
33 | transition: all .5s;
34 | }
35 |
36 | .breadcrumb-enter,
37 | .breadcrumb-leave-active {
38 | opacity: 0;
39 | transform: translateX(20px);
40 | }
41 |
42 | .breadcrumb-move {
43 | transition: all .5s;
44 | }
45 |
46 | .breadcrumb-leave-active {
47 | position: absolute;
48 | }
49 |
--------------------------------------------------------------------------------
/vue-admin/build/index.js:
--------------------------------------------------------------------------------
1 | const { run } = require('runjs')
2 | const chalk = require('chalk')
3 | const config = require('../vue.config.js')
4 | const rawArgv = process.argv.slice(2)
5 | const args = rawArgv.join(' ')
6 |
7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
8 | const report = rawArgv.includes('--report')
9 |
10 | run(`vue-cli-service build ${args}`)
11 |
12 | const port = 9526
13 | const publicPath = config.publicPath
14 |
15 | var connect = require('connect')
16 | var serveStatic = require('serve-static')
17 | const app = connect()
18 |
19 | app.use(
20 | publicPath,
21 | serveStatic('./dist', {
22 | index: ['index.html', '/']
23 | })
24 | )
25 |
26 | app.listen(port, function () {
27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
28 | if (report) {
29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
30 | }
31 |
32 | })
33 | } else {
34 | run(`vue-cli-service build ${args}`)
35 | }
36 |
--------------------------------------------------------------------------------
/routers/api/v2/article.go:
--------------------------------------------------------------------------------
1 | package v2
2 |
3 | import (
4 | "gin-vue/pkg/e"
5 | "gin-vue/pkg/setting"
6 | "gin-vue/pkg/util"
7 | "net/http"
8 |
9 | "github.com/gin-gonic/gin"
10 |
11 | "gin-vue/models"
12 | "gin-vue/viewModels"
13 | "gin-vue/viewModels/emun"
14 | )
15 |
16 | //获取多个文章
17 | func GetArticles(c *gin.Context) {
18 | maps := make(map[string]interface{})
19 | code := e.SUCCESS
20 | var viewArticles []viewModels.Article
21 | var viewArticle viewModels.Article
22 | articles := models.GetArticles(util.GetPage(c), setting.PageSize, maps)
23 | for _, articles := range articles {
24 | viewArticle.Id = articles.ID
25 | viewArticle.Author = articles.CreatedBy
26 | viewArticle.DisplayTime = articles.ModifiedOn.String()
27 | viewArticle.Pageviews = 3474
28 | viewArticle.Status = emun.GetArticleStatus(articles.State)
29 | viewArticle.Title = articles.Title
30 | viewArticles = append(viewArticles, viewArticle)
31 | }
32 | c.JSON(http.StatusOK, gin.H{
33 | "code": code,
34 | "msg": e.GetMsg(code),
35 | "data": viewArticles,
36 | })
37 | }
38 |
--------------------------------------------------------------------------------
/models/auth.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type Auth struct {
4 | ID int `gorm:"primary_key" json:"id"`
5 | Username string `json:"username"`
6 | Password string `json:"password"`
7 | Avatar string `json:"avatar"`
8 | }
9 |
10 | // User demo
11 | type User struct {
12 | UserName string
13 | UserClaims []Claims
14 | }
15 |
16 | func CheckAuth(username, password string) bool {
17 | var auth Auth
18 | db.Select("id").Where(Auth{Username: username, Password: password}).First(&auth)
19 | if auth.ID > 0 {
20 | return true
21 | }
22 |
23 | return false
24 | }
25 |
26 | func GetUserID(username string) string {
27 | var auth Auth
28 | db.Select("avatar").Where(Auth{Username: username}).First(&auth)
29 | return auth.Avatar
30 | }
31 |
32 | func GetRoles(username string) []string {
33 | var auth Auth
34 | db.Select("id").Where(Auth{Username: username}).First(&auth)
35 | var claims []Claims
36 | db.Select("value").Where(Claims{AuthID: auth.ID}).Find(&claims)
37 | var roles []string
38 | for _, claim := range claims {
39 | roles = append(roles, claim.Value)
40 | }
41 | return roles
42 | }
43 |
--------------------------------------------------------------------------------
/vue-admin/tests/unit/utils/parseTime.spec.js:
--------------------------------------------------------------------------------
1 | import { parseTime } from '@/utils/index.js'
2 |
3 | describe('Utils:parseTime', () => {
4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5 | it('timestamp', () => {
6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01')
7 | })
8 | it('ten digits timestamp', () => {
9 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
10 | })
11 | it('new Date', () => {
12 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
13 | })
14 | it('format', () => {
15 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
16 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
17 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
18 | })
19 | it('get the day of the week', () => {
20 | expect(parseTime(d, '{a}')).toBe('五') // 星期五
21 | })
22 | it('get the day of the week', () => {
23 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
24 | })
25 | it('empty argument', () => {
26 | expect(parseTime()).toBeNull()
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/vue-admin/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 store from './store'
13 | import router from './router'
14 |
15 | import '@/icons' // icon
16 | import '@/permission' // permission control
17 |
18 | /**
19 | * If you don't want to use mock-server
20 | * you want to use MockJs for mock api
21 | * you can execute: mockXHR()
22 | *
23 | * Currently MockJs will be used in the production environment,
24 | * please remove it before going online! ! !
25 | */
26 | import { mockXHR } from '../mock'
27 | if (process.env.NODE_ENV === 'production') {
28 | mockXHR()
29 | }
30 |
31 | // set ElementUI lang to EN
32 | Vue.use(ElementUI, { locale })
33 |
34 | Vue.config.productionTip = false
35 |
36 | new Vue({
37 | el: '#app',
38 | router,
39 | store,
40 | render: h => h(App)
41 | })
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 zhubingjian
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 |
--------------------------------------------------------------------------------
/vue-admin/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 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/skill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/tests/unit/utils/formatTime.spec.js:
--------------------------------------------------------------------------------
1 | import { formatTime } from '@/utils/index.js'
2 |
3 | describe('Utils:formatTime', () => {
4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5 | const retrofit = 5 * 1000
6 |
7 | it('ten digits timestamp', () => {
8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
9 | })
10 | it('test now', () => {
11 | expect(formatTime(+new Date() - 1)).toBe('刚刚')
12 | })
13 | it('less two minute', () => {
14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
15 | })
16 | it('less two hour', () => {
17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
18 | })
19 | it('less one day', () => {
20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
21 | })
22 | it('more than one day', () => {
23 | expect(formatTime(d)).toBe('7月13日17时54分')
24 | })
25 | it('format', () => {
26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/vue-admin/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:focus,
35 | a:active {
36 | outline: none;
37 | }
38 |
39 | a,
40 | a:focus,
41 | a:hover {
42 | cursor: pointer;
43 | color: inherit;
44 | text-decoration: none;
45 | }
46 |
47 | div:focus {
48 | outline: none;
49 | }
50 |
51 | .clearfix {
52 | &:after {
53 | visibility: hidden;
54 | display: block;
55 | font-size: 0;
56 | content: " ";
57 | clear: both;
58 | height: 0;
59 | }
60 | }
61 |
62 | // main-container global css
63 | .app-container {
64 | padding: 20px;
65 | }
66 |
--------------------------------------------------------------------------------
/middleware/myjwt/myjwt.go:
--------------------------------------------------------------------------------
1 | package myjwt
2 |
3 | import (
4 | "gin-vue/models"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | //IAuthorizator 授权规则接口
10 | type IAuthorizator interface {
11 | HandleAuthorizator(data interface{}, c *gin.Context) bool
12 | }
13 |
14 | //AdminAuthorizator 管理员授权规则
15 | type AdminAuthorizator struct {
16 | }
17 |
18 | //HandleAuthorizator 处理管理员授权规则
19 | func (*AdminAuthorizator) HandleAuthorizator(data interface{}, c *gin.Context) bool {
20 | if v, ok := data.(*models.User); ok {
21 | for _, itemClaim := range v.UserClaims {
22 | if itemClaim.Type == "role" && itemClaim.Value == "admin" {
23 | return true
24 | }
25 | }
26 | }
27 | return false
28 | }
29 |
30 | //TestAuthorizator 测试用户授权规则
31 | type TestAuthorizator struct {
32 | }
33 |
34 | //HandleAuthorizator 处理测试用户授权规则
35 | func (*TestAuthorizator) HandleAuthorizator(data interface{}, c *gin.Context) bool {
36 | if v, ok := data.(*models.User); ok && v.UserName == "test" {
37 | return true
38 | }
39 | return false
40 | }
41 |
42 | //AllUserAuthorizator 普通用户授权规则
43 | type AllUserAuthorizator struct {
44 | }
45 |
46 | //HandleAuthorizator 处理普通用户授权规则
47 | func (*AllUserAuthorizator) HandleAuthorizator(data interface{}, c *gin.Context) bool {
48 | return true
49 | }
50 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/src/store/modules/app.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const state = {
4 | sidebar: {
5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
6 | withoutAnimation: false
7 | },
8 | device: 'desktop'
9 | }
10 |
11 | const mutations = {
12 | TOGGLE_SIDEBAR: state => {
13 | state.sidebar.opened = !state.sidebar.opened
14 | state.sidebar.withoutAnimation = false
15 | if (state.sidebar.opened) {
16 | Cookies.set('sidebarStatus', 1)
17 | } else {
18 | Cookies.set('sidebarStatus', 0)
19 | }
20 | },
21 | CLOSE_SIDEBAR: (state, withoutAnimation) => {
22 | Cookies.set('sidebarStatus', 0)
23 | state.sidebar.opened = false
24 | state.sidebar.withoutAnimation = withoutAnimation
25 | },
26 | TOGGLE_DEVICE: (state, device) => {
27 | state.device = device
28 | }
29 | }
30 |
31 | const actions = {
32 | toggleSideBar({ commit }) {
33 | commit('TOGGLE_SIDEBAR')
34 | },
35 | closeSideBar({ commit }, { withoutAnimation }) {
36 | commit('CLOSE_SIDEBAR', withoutAnimation)
37 | },
38 | toggleDevice({ commit }, device) {
39 | commit('TOGGLE_DEVICE', device)
40 | }
41 | }
42 |
43 | export default {
44 | namespaced: true,
45 | state,
46 | mutations,
47 | actions
48 | }
49 |
--------------------------------------------------------------------------------
/vue-admin/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
32 |
33 |
45 |
--------------------------------------------------------------------------------
/pkg/setting/setting.go:
--------------------------------------------------------------------------------
1 | package setting
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "github.com/go-ini/ini"
8 | )
9 |
10 | var (
11 | Cfg *ini.File
12 |
13 | RunMode string
14 |
15 | HTTPPort int
16 | ReadTimeout time.Duration
17 | WriteTimeout time.Duration
18 |
19 | PageSize int
20 | IdentityKey string
21 | )
22 |
23 | func init() {
24 | var err error
25 | Cfg, err = ini.Load("conf/app.ini")
26 | if err != nil {
27 | log.Fatalf("Fail to parse 'conf/app.ini': %v", err)
28 | }
29 |
30 | LoadBase()
31 | LoadServer()
32 | LoadApp()
33 | }
34 |
35 | func LoadBase() {
36 | RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
37 | }
38 |
39 | func LoadServer() {
40 | sec, err := Cfg.GetSection("server")
41 | if err != nil {
42 | log.Fatalf("Fail to get section 'server': %v", err)
43 | }
44 |
45 | RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
46 |
47 | HTTPPort = sec.Key("HTTP_PORT").MustInt(8000)
48 | ReadTimeout = time.Duration(sec.Key("READ_TIMEOUT").MustInt(60)) * time.Second
49 | WriteTimeout = time.Duration(sec.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second
50 | }
51 |
52 | func LoadApp() {
53 | sec, err := Cfg.GetSection("app")
54 | if err != nil {
55 | log.Fatalf("Fail to get section 'app': %v", err)
56 | }
57 |
58 | IdentityKey = sec.Key("IDENTITY_KEY").MustString("!@)*#)!@U#@*!@!)")
59 | PageSize = sec.Key("PAGE_SIZE").MustInt(10)
60 | }
61 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/mixin/ResizeHandler.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | const { body } = document
4 | const WIDTH = 992 // refer to Bootstrap's responsive design
5 |
6 | export default {
7 | watch: {
8 | $route(route) {
9 | if (this.device === 'mobile' && this.sidebar.opened) {
10 | store.dispatch('app/closeSideBar', { withoutAnimation: false })
11 | }
12 | }
13 | },
14 | beforeMount() {
15 | window.addEventListener('resize', this.$_resizeHandler)
16 | },
17 | beforeDestroy() {
18 | window.removeEventListener('resize', this.$_resizeHandler)
19 | },
20 | mounted() {
21 | const isMobile = this.$_isMobile()
22 | if (isMobile) {
23 | store.dispatch('app/toggleDevice', 'mobile')
24 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
25 | }
26 | },
27 | methods: {
28 | // use $_ for mixins properties
29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
30 | $_isMobile() {
31 | const rect = body.getBoundingClientRect()
32 | return rect.width - 1 < WIDTH
33 | },
34 | $_resizeHandler() {
35 | if (!document.hidden) {
36 | const isMobile = this.$_isMobile()
37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
38 |
39 | if (isMobile) {
40 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/models/models.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "time"
7 |
8 | "github.com/jinzhu/gorm"
9 | _ "github.com/jinzhu/gorm/dialects/mysql"
10 |
11 | "gin-vue/pkg/setting"
12 | )
13 |
14 | var db *gorm.DB
15 |
16 | type Model struct {
17 | ID int `gorm:"primary_key" json:"id"`
18 | CreatedOn time.Time `json:"created_on"`
19 | ModifiedOn time.Time `json:"modified_on"`
20 | }
21 |
22 | func init() {
23 | var (
24 | err error
25 | dbType, dbName, user, password, host, tablePrefix string
26 | )
27 |
28 | sec, err := setting.Cfg.GetSection("database")
29 | if err != nil {
30 | log.Fatal(2, "Fail to get section 'database': %v", err)
31 | }
32 |
33 | dbType = sec.Key("TYPE").String()
34 | dbName = sec.Key("NAME").String()
35 | user = sec.Key("USER").String()
36 | password = sec.Key("PASSWORD").String()
37 | host = sec.Key("HOST").String()
38 | tablePrefix = sec.Key("TABLE_PREFIX").String()
39 |
40 | db, err = gorm.Open(dbType, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
41 | user,
42 | password,
43 | host,
44 | dbName))
45 |
46 | if err != nil {
47 | log.Println(err)
48 | }
49 |
50 | gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {
51 | return tablePrefix + defaultTableName
52 | }
53 |
54 | db.SingularTable(true)
55 | db.DB().SetMaxIdleConns(10)
56 | db.DB().SetMaxOpenConns(100)
57 | }
58 |
59 | func CloseDB() {
60 | defer db.Close()
61 | }
62 |
--------------------------------------------------------------------------------
/vue-admin/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
47 |
48 |
63 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/pdf.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
57 |
--------------------------------------------------------------------------------
/models/tag.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/jinzhu/gorm"
7 | )
8 |
9 | type Tag struct {
10 | Model
11 |
12 | Name string `json:"name"`
13 | CreatedBy string `json:"created_by"`
14 | ModifiedBy string `json:"modified_by"`
15 | State int `json:"state"`
16 | }
17 |
18 | func GetTags(pageNum int, pageSize int, maps interface{}) (tags []Tag) {
19 | db.Where(maps).Offset(pageNum).Limit(pageSize).Find(&tags)
20 |
21 | return
22 | }
23 |
24 | func GetTagTotal(maps interface{}) (count int) {
25 | db.Model(&Tag{}).Where(maps).Count(&count)
26 |
27 | return
28 | }
29 |
30 | func ExistTagByName(name string) bool {
31 | var tag Tag
32 | db.Select("id").Where("name = ?", name).First(&tag)
33 | if tag.ID > 0 {
34 | return true
35 | }
36 |
37 | return false
38 | }
39 |
40 | func AddTag(name string, state int, createdBy string) bool {
41 | db.Create(&Tag{
42 | Name: name,
43 | State: state,
44 | CreatedBy: createdBy,
45 | })
46 |
47 | return true
48 | }
49 |
50 | func (tag *Tag) BeforeCreate(scope *gorm.Scope) error {
51 | scope.SetColumn("CreatedOn", time.Now().Unix())
52 |
53 | return nil
54 | }
55 |
56 | func (tag *Tag) BeforeUpdate(scope *gorm.Scope) error {
57 | scope.SetColumn("ModifiedOn", time.Now().Unix())
58 |
59 | return nil
60 | }
61 |
62 | func ExistTagByID(id int) bool {
63 | var tag Tag
64 | db.Select("id").Where("id = ?", id).First(&tag)
65 | if tag.ID > 0 {
66 | return true
67 | }
68 |
69 | return false
70 | }
71 |
72 | func DeleteTag(id int) bool {
73 | db.Where("id = ?", id).Delete(&Tag{})
74 |
75 | return true
76 | }
77 |
78 | func EditTag(id int, data interface{}) bool {
79 | db.Model(&Tag{}).Where("id = ?", id).Updates(data)
80 |
81 | return true
82 | }
83 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/src/views/tree/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
16 |
17 |
78 |
79 |
--------------------------------------------------------------------------------
/vue-admin/mock/user.js:
--------------------------------------------------------------------------------
1 |
2 | const tokens = {
3 | admin: {
4 | token: 'admin-token'
5 | },
6 | editor: {
7 | token: 'editor-token'
8 | }
9 | }
10 |
11 | const users = {
12 | 'admin-token': {
13 | roles: ['admin'],
14 | introduction: 'I am a super administrator',
15 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
16 | name: 'Super Admin'
17 | },
18 | 'editor-token': {
19 | roles: ['editor'],
20 | introduction: 'I am an editor',
21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
22 | name: 'Normal Editor'
23 | }
24 | }
25 |
26 | export default [
27 | // user login
28 | {
29 | url: '/user/login',
30 | type: 'post',
31 | response: config => {
32 | const { username } = config.body
33 | const token = tokens[username]
34 |
35 | // mock error
36 | if (!token) {
37 | return {
38 | code: 60204,
39 | message: 'Account and password are incorrect.'
40 | }
41 | }
42 |
43 | return {
44 | code: 20000,
45 | data: token
46 | }
47 | }
48 | },
49 |
50 | // get user info
51 | {
52 | url: '/user/info\.*',
53 | type: 'get',
54 | response: config => {
55 | const { token } = config.query
56 | const info = users[token]
57 |
58 | // mock error
59 | if (!info) {
60 | return {
61 | code: 50008,
62 | message: 'Login failed, unable to get user details.'
63 | }
64 | }
65 |
66 | return {
67 | code: 20000,
68 | data: info
69 | }
70 | }
71 | },
72 |
73 | // user logout
74 | {
75 | url: '/user/logout',
76 | type: 'post',
77 | response: _ => {
78 | return {
79 | code: 20000,
80 | data: 'success'
81 | }
82 | }
83 | }
84 | ]
85 |
--------------------------------------------------------------------------------
/vue-admin/mock/index.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 | import { param2Obj } from '../src/utils'
3 |
4 | import user from './user'
5 | import table from './table'
6 |
7 | const mocks = [
8 | ...user,
9 | ...table
10 | ]
11 |
12 | // for front mock
13 | // please use it cautiously, it will redefine XMLHttpRequest,
14 | // which will cause many of your third-party libraries to be invalidated(like progress event).
15 | export function mockXHR() {
16 | // mock patch
17 | // https://github.com/nuysoft/Mock/issues/300
18 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
19 | Mock.XHR.prototype.send = function() {
20 | if (this.custom.xhr) {
21 | this.custom.xhr.withCredentials = this.withCredentials || false
22 |
23 | if (this.responseType) {
24 | this.custom.xhr.responseType = this.responseType
25 | }
26 | }
27 | this.proxy_send(...arguments)
28 | }
29 |
30 | function XHR2ExpressReqWrap(respond) {
31 | return function(options) {
32 | let result = null
33 | if (respond instanceof Function) {
34 | const { body, type, url } = options
35 | // https://expressjs.com/en/4x/api.html#req
36 | result = respond({
37 | method: type,
38 | body: JSON.parse(body),
39 | query: param2Obj(url)
40 | })
41 | } else {
42 | result = respond
43 | }
44 | return Mock.mock(result)
45 | }
46 | }
47 |
48 | for (const i of mocks) {
49 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
50 | }
51 | }
52 |
53 | // for mock server
54 | const responseFake = (url, type, respond) => {
55 | return {
56 | url: new RegExp(`/mock${url}`),
57 | type: type || 'get',
58 | response(req, res) {
59 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
60 | }
61 | }
62 | }
63 |
64 | export default mocks.map(route => {
65 | return responseFake(route.url, route.type, route.response)
66 | })
67 |
--------------------------------------------------------------------------------
/vue-admin/src/permission.js:
--------------------------------------------------------------------------------
1 | import router from './router'
2 | import store from './store'
3 | import { Message } from 'element-ui'
4 | import NProgress from 'nprogress' // progress bar
5 | import 'nprogress/nprogress.css' // progress bar style
6 | import { getToken } from '@/utils/auth' // get token from cookie
7 | import getPageTitle from '@/utils/get-page-title'
8 |
9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration
10 |
11 | const whiteList = ['/login'] // no redirect whitelist
12 |
13 | router.beforeEach(async(to, from, next) => {
14 | // start progress bar
15 | NProgress.start()
16 |
17 | // set page title
18 | document.title = getPageTitle(to.meta.title)
19 |
20 | // determine whether the user has logged in
21 | const hasToken = getToken()
22 |
23 | if (hasToken) {
24 | if (to.path === '/login') {
25 | // if is logged in, redirect to the home page
26 | next({ path: '/' })
27 | NProgress.done()
28 | } else {
29 | const hasGetUserInfo = store.getters.name
30 | if (hasGetUserInfo) {
31 | next()
32 | } else {
33 | try {
34 | // get user info
35 | await store.dispatch('user/getInfo')
36 | next()
37 | } catch (error) {
38 | // remove token and go to login page to re-login
39 | await store.dispatch('user/resetToken')
40 | Message.error(error || 'Has Error')
41 | next(`/login?redirect=${to.path}`)
42 | NProgress.done()
43 | }
44 | }
45 | }
46 | } else {
47 | /* has no token*/
48 |
49 | if (whiteList.indexOf(to.path) !== -1) {
50 | // in the free login whitelist, go directly
51 | next()
52 | } else {
53 | // other pages that do not have permission to access are redirected to the login page.
54 | next(`/login?redirect=${to.path}`)
55 | NProgress.done()
56 | }
57 | }
58 | })
59 |
60 | router.afterEach(() => {
61 | // finish progress bar
62 | NProgress.done()
63 | })
64 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
33 |
34 |
83 |
--------------------------------------------------------------------------------
/vue-admin/mock/mock-server.js:
--------------------------------------------------------------------------------
1 | const chokidar = require('chokidar')
2 | const bodyParser = require('body-parser')
3 | const chalk = require('chalk')
4 | const path = require('path')
5 |
6 | const mockDir = path.join(process.cwd(), 'mock')
7 |
8 | function registerRoutes(app) {
9 | let mockLastIndex
10 | const { default: mocks } = require('./index.js')
11 | for (const mock of mocks) {
12 | app[mock.type](mock.url, mock.response)
13 | mockLastIndex = app._router.stack.length
14 | }
15 | const mockRoutesLength = Object.keys(mocks).length
16 | return {
17 | mockRoutesLength: mockRoutesLength,
18 | mockStartIndex: mockLastIndex - mockRoutesLength
19 | }
20 | }
21 |
22 | function unregisterRoutes() {
23 | Object.keys(require.cache).forEach(i => {
24 | if (i.includes(mockDir)) {
25 | delete require.cache[require.resolve(i)]
26 | }
27 | })
28 | }
29 |
30 | module.exports = app => {
31 | // es6 polyfill
32 | require('@babel/register')
33 |
34 | // parse app.body
35 | // https://expressjs.com/en/4x/api.html#req.body
36 | app.use(bodyParser.json())
37 | app.use(bodyParser.urlencoded({
38 | extended: true
39 | }))
40 |
41 | const mockRoutes = registerRoutes(app)
42 | var mockRoutesLength = mockRoutes.mockRoutesLength
43 | var mockStartIndex = mockRoutes.mockStartIndex
44 |
45 | // watch files, hot reload mock server
46 | chokidar.watch(mockDir, {
47 | ignored: /mock-server/,
48 | ignoreInitial: true
49 | }).on('all', (event, path) => {
50 | if (event === 'change' || event === 'add') {
51 | try {
52 | // remove mock routes stack
53 | app._router.stack.splice(mockStartIndex, mockRoutesLength)
54 |
55 | // clear routes cache
56 | unregisterRoutes()
57 |
58 | const mockRoutes = registerRoutes(app)
59 | mockRoutesLength = mockRoutes.mockRoutesLength
60 | mockStartIndex = mockRoutes.mockStartIndex
61 |
62 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
63 | } catch (error) {
64 | console.log(chalk.redBright(error))
65 | }
66 | }
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/vue-admin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-admin-template",
3 | "version": "4.2.1",
4 | "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
5 | "author": "Pan ",
6 | "license": "MIT",
7 | "scripts": {
8 | "dev": "vue-cli-service serve",
9 | "build:prod": "vue-cli-service build",
10 | "build:stage": "vue-cli-service build --mode staging",
11 | "preview": "node build/index.js --preview",
12 | "lint": "eslint --ext .js,.vue src",
13 | "test:unit": "jest --clearCache && vue-cli-service test:unit",
14 | "test:ci": "npm run lint && npm run test:unit",
15 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
16 | },
17 | "dependencies": {
18 | "axios": "0.18.1",
19 | "element-ui": "2.7.2",
20 | "js-cookie": "2.2.0",
21 | "normalize.css": "7.0.0",
22 | "nprogress": "0.2.0",
23 | "path-to-regexp": "2.4.0",
24 | "vue": "2.6.10",
25 | "vue-router": "3.0.6",
26 | "vuex": "3.1.0"
27 | },
28 | "devDependencies": {
29 | "@babel/core": "7.0.0",
30 | "@babel/register": "7.0.0",
31 | "@vue/cli-plugin-babel": "3.6.0",
32 | "@vue/cli-plugin-eslint": "^3.9.1",
33 | "@vue/cli-plugin-unit-jest": "3.6.3",
34 | "@vue/cli-service": "3.6.0",
35 | "@vue/test-utils": "1.0.0-beta.29",
36 | "autoprefixer": "^9.5.1",
37 | "babel-core": "7.0.0-bridge.0",
38 | "babel-eslint": "10.0.1",
39 | "babel-jest": "23.6.0",
40 | "chalk": "2.4.2",
41 | "connect": "3.6.6",
42 | "eslint": "5.15.3",
43 | "eslint-plugin-vue": "5.2.2",
44 | "html-webpack-plugin": "3.2.0",
45 | "mockjs": "1.0.1-beta3",
46 | "node-sass": "^4.9.0",
47 | "runjs": "^4.3.2",
48 | "sass-loader": "^7.1.0",
49 | "script-ext-html-webpack-plugin": "2.1.3",
50 | "script-loader": "0.7.2",
51 | "serve-static": "^1.13.2",
52 | "svg-sprite-loader": "4.1.3",
53 | "svgo": "1.2.2",
54 | "vue-template-compiler": "2.6.10"
55 | },
56 | "engines": {
57 | "node": ">=8.9",
58 | "npm": ">= 3.0.0"
59 | },
60 | "browserslist": [
61 | "> 1%",
62 | "last 2 versions"
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/models/article.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/jinzhu/gorm"
7 | )
8 |
9 | type Article struct {
10 | Model
11 |
12 | State int `json:"state"`
13 | TagId int `json:"tag_id"`
14 | Title string `json:"title"`
15 | Desc string `json:"desc"`
16 | Content string `json:"Content"`
17 | CoverImageUrl string `json:"cover_image_url"`
18 | CreatedBy string `json:"created_by"`
19 | Tag Tag `json:"tag"`
20 | }
21 |
22 | func (tag *Article) BeforeCreate(scope *gorm.Scope) error {
23 | scope.SetColumn("CreatedOn", time.Now().Unix())
24 |
25 | return nil
26 | }
27 |
28 | func (tag *Article) BeforeUpdate(scope *gorm.Scope) error {
29 | scope.SetColumn("ModifiedOn", time.Now().Unix())
30 |
31 | return nil
32 | }
33 |
34 | func GetArticle(id int) (article Article) {
35 | db.Where("id = ?", id).First(&article)
36 | db.Where("id = ?", article.TagId).First(&article.Tag)
37 | //db.Model(&article).Related(&article.Tag)
38 | return
39 | }
40 |
41 | func GetArticles(PageNum int, PageSize int, maps interface{}) (article []Article) {
42 | db.Where(maps).Offset(PageNum).Limit(PageSize).Find(&article)
43 | return
44 | }
45 |
46 | func AddArticle(data map[string]interface{}) bool {
47 | db.Create(&Article{
48 | TagId: data["tag_id"].(int),
49 | Title: data["title"].(string),
50 | Desc: data["desc"].(string),
51 | Content: data["content"].(string),
52 | CreatedBy: data["created_by"].(string),
53 | State: data["state"].(int),
54 | })
55 |
56 | return true
57 | }
58 |
59 | func EditArticle(id int, maps interface{}) bool {
60 | db.Model(&Article{}).Where("id = ?", id).Update(maps)
61 | return true
62 | }
63 |
64 | func DeleteArticle(id int) bool {
65 | db.Where("id = ?", id).Delete(&Article{})
66 | return true
67 | }
68 |
69 | func ExistArticleByID(id int) bool {
70 | var article Article
71 | db.Select("id").Where("id = ?", id).First(&article)
72 | if article.ID > 0 {
73 | return true
74 | }
75 | return false
76 | }
77 |
78 | func GetArticleTotal(maps interface{}) (count int) {
79 | db.Model(&Article{}).Where(maps).Count(&count)
80 | return
81 | }
82 |
--------------------------------------------------------------------------------
/routers/router.go:
--------------------------------------------------------------------------------
1 | package routers
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 |
6 | "gin-vue/middleware/cors"
7 | "gin-vue/middleware/myjwt"
8 | "gin-vue/pkg/e"
9 | "gin-vue/pkg/setting"
10 | v1 "gin-vue/routers/api/v1"
11 | v2 "gin-vue/routers/api/v2"
12 | )
13 |
14 | func InitRouter() *gin.Engine {
15 | r := gin.New()
16 | r.Use(gin.Logger())
17 | r.Use(cors.CorsHandler())
18 | r.Use(gin.Recovery())
19 | gin.SetMode(setting.RunMode)
20 | var authMiddleware = myjwt.GinJWTMiddlewareInit(&myjwt.AllUserAuthorizator{})
21 | r.POST("/login", authMiddleware.LoginHandler)
22 | //404 handler
23 | r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
24 | code := e.PAGE_NOT_FOUND
25 | c.JSON(404, gin.H{"code": code, "message": e.GetMsg(code)})
26 | })
27 | auth := r.Group("/auth")
28 | {
29 | // Refresh time can be longer than token timeout
30 | auth.GET("/refresh_token", authMiddleware.RefreshHandler)
31 | }
32 |
33 | api := r.Group("/user")
34 | api.Use(authMiddleware.MiddlewareFunc())
35 | {
36 | api.GET("/info", v1.GetUserInfo)
37 | api.POST("/logout", v1.Logout)
38 | }
39 |
40 | var adminMiddleware = myjwt.GinJWTMiddlewareInit(&myjwt.AdminAuthorizator{})
41 | apiv1 := r.Group("/api/v1")
42 | //使用AdminAuthorizator中间件,只有admin权限的用户才能获取到接口
43 | apiv1.Use(adminMiddleware.MiddlewareFunc())
44 | {
45 | //vue获取table信息
46 | apiv1.GET("/table/list", v2.GetArticles)
47 | //获取标签列表
48 | apiv1.GET("/tags", v1.GetTags)
49 | //新建标签
50 | apiv1.POST("/tags", v1.AddTag)
51 | //更新指定标签
52 | apiv1.PUT("/tags/:id", v1.EditTag)
53 | //删除指定标签
54 | apiv1.DELETE("/tags/:id", v1.DeleteTag)
55 |
56 | //获取文章列表
57 | apiv1.GET("/articles", v1.GetArticles)
58 | //获取指定文章
59 | apiv1.GET("/articles/:id", v1.GetArticle)
60 | //新建文章
61 | apiv1.POST("/articles", v1.AddArticle)
62 | //更新指定文章
63 | apiv1.PUT("/articles/:id", v1.EditArticle)
64 | //删除指定文章
65 | apiv1.DELETE("/articles/:id", v1.DeleteArticle)
66 | }
67 |
68 | var testMiddleware = myjwt.GinJWTMiddlewareInit(&myjwt.TestAuthorizator{})
69 | apiv2 := r.Group("/api/v2")
70 | apiv2.Use(testMiddleware.MiddlewareFunc())
71 | {
72 | //获取文章列表
73 | apiv2.GET("/articles", v2.GetArticles)
74 | }
75 |
76 | return r
77 | }
78 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
52 |
53 |
94 |
--------------------------------------------------------------------------------
/vue-admin/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ item.meta.title }}
6 | {{ item.meta.title }}
7 |
8 |
9 |
10 |
11 |
12 |
65 |
66 |
79 |
--------------------------------------------------------------------------------
/vue-admin/src/views/table/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 | {{ scope.$index+1 }}
14 |
15 |
16 |
17 |
18 | {{ scope.row.Title }}
19 |
20 |
21 |
22 |
23 | {{ scope.row.Author }}
24 |
25 |
26 |
27 |
28 | {{ scope.row.Pageviews }}
29 |
30 |
31 |
32 |
33 | {{ scope.row.Status }}
34 |
35 |
36 |
37 |
38 |
39 | {{ scope.row.DisplayTime }}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
80 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/middleware/cors/cors.go:
--------------------------------------------------------------------------------
1 | package cors
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | func CorsHandler() gin.HandlerFunc {
12 | return func(c *gin.Context) {
13 | method := c.Request.Method //请求方法
14 | origin := c.Request.Header.Get("Origin") //请求头部
15 | var headerKeys []string // 声明请求头keys
16 | for k, _ := range c.Request.Header {
17 | headerKeys = append(headerKeys, k)
18 | }
19 | headerStr := strings.Join(headerKeys, ", ")
20 | if headerStr != "" {
21 | headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
22 | } else {
23 | headerStr = "access-control-allow-origin, access-control-allow-headers"
24 | }
25 | if origin != "" {
26 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
27 | c.Header("Access-Control-Allow-Origin", "*") // 这是允许访问所有域
28 | c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") //服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求
29 | // header的类型
30 | c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
31 | // 允许跨域设置 可以返回其他子段
32 | c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析
33 | c.Header("Access-Control-Max-Age", "172800") // 缓存请求信息 单位为秒
34 | c.Header("Access-Control-Allow-Credentials", "false") // 跨域请求是否需要带cookie信息 默认设置为true
35 | c.Set("content-type", "application/json") // 设置返回格式是json
36 | }
37 |
38 | //放行所有OPTIONS方法
39 | if method == "OPTIONS" {
40 | c.JSON(http.StatusOK, "Options Request!")
41 | }
42 | // 处理请求
43 | c.Next() // 处理请求
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/vue-admin/src/views/403.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 返回
5 |
6 |
7 |
8 |
9 | Oops!
10 |
11 | gif来源airbnb 页面
12 | 你没有权限去该页面
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 |
60 |
61 |
100 |
--------------------------------------------------------------------------------
/vue-admin/src/views/form/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Create
44 | Cancel
45 |
46 |
47 |
48 |
49 |
50 |
79 |
80 |
85 |
86 |
--------------------------------------------------------------------------------
/vue-admin/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * Parse the time to string
7 | * @param {(Object|string|number)} time
8 | * @param {string} cFormat
9 | * @returns {string}
10 | */
11 | export function parseTime(time, cFormat) {
12 | if (arguments.length === 0) {
13 | return null
14 | }
15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
16 | let date
17 | if (typeof time === 'object') {
18 | date = time
19 | } else {
20 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
21 | time = parseInt(time)
22 | }
23 | if ((typeof time === 'number') && (time.toString().length === 10)) {
24 | time = time * 1000
25 | }
26 | date = new Date(time)
27 | }
28 | const formatObj = {
29 | y: date.getFullYear(),
30 | m: date.getMonth() + 1,
31 | d: date.getDate(),
32 | h: date.getHours(),
33 | i: date.getMinutes(),
34 | s: date.getSeconds(),
35 | a: date.getDay()
36 | }
37 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
38 | let value = formatObj[key]
39 | // Note: getDay() returns 0 on Sunday
40 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
41 | if (result.length > 0 && value < 10) {
42 | value = '0' + value
43 | }
44 | return value || 0
45 | })
46 | return time_str
47 | }
48 |
49 | /**
50 | * @param {number} time
51 | * @param {string} option
52 | * @returns {string}
53 | */
54 | export function formatTime(time, option) {
55 | if (('' + time).length === 10) {
56 | time = parseInt(time) * 1000
57 | } else {
58 | time = +time
59 | }
60 | const d = new Date(time)
61 | const now = Date.now()
62 |
63 | const diff = (now - d) / 1000
64 |
65 | if (diff < 30) {
66 | return '刚刚'
67 | } else if (diff < 3600) {
68 | // less 1 hour
69 | return Math.ceil(diff / 60) + '分钟前'
70 | } else if (diff < 3600 * 24) {
71 | return Math.ceil(diff / 3600) + '小时前'
72 | } else if (diff < 3600 * 24 * 2) {
73 | return '1天前'
74 | }
75 | if (option) {
76 | return parseTime(time, option)
77 | } else {
78 | return (
79 | d.getMonth() +
80 | 1 +
81 | '月' +
82 | d.getDate() +
83 | '日' +
84 | d.getHours() +
85 | '时' +
86 | d.getMinutes() +
87 | '分'
88 | )
89 | }
90 | }
91 |
92 | /**
93 | * @param {string} url
94 | * @returns {Object}
95 | */
96 | export function param2Obj(url) {
97 | const search = url.split('?')[1]
98 | if (!search) {
99 | return {}
100 | }
101 | return JSON.parse(
102 | '{"' +
103 | decodeURIComponent(search)
104 | .replace(/"/g, '\\"')
105 | .replace(/&/g, '","')
106 | .replace(/=/g, '":"')
107 | .replace(/\+/g, ' ') +
108 | '"}'
109 | )
110 | }
111 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
96 |
--------------------------------------------------------------------------------
/vue-admin/tests/unit/components/Breadcrumb.spec.js:
--------------------------------------------------------------------------------
1 | import { mount, createLocalVue } from '@vue/test-utils'
2 | import VueRouter from 'vue-router'
3 | import ElementUI from 'element-ui'
4 | import Breadcrumb from '@/components/Breadcrumb/index.vue'
5 |
6 | const localVue = createLocalVue()
7 | localVue.use(VueRouter)
8 | localVue.use(ElementUI)
9 |
10 | const routes = [
11 | {
12 | path: '/',
13 | name: 'home',
14 | children: [{
15 | path: 'dashboard',
16 | name: 'dashboard'
17 | }]
18 | },
19 | {
20 | path: '/menu',
21 | name: 'menu',
22 | children: [{
23 | path: 'menu1',
24 | name: 'menu1',
25 | meta: { title: 'menu1' },
26 | children: [{
27 | path: 'menu1-1',
28 | name: 'menu1-1',
29 | meta: { title: 'menu1-1' }
30 | },
31 | {
32 | path: 'menu1-2',
33 | name: 'menu1-2',
34 | redirect: 'noredirect',
35 | meta: { title: 'menu1-2' },
36 | children: [{
37 | path: 'menu1-2-1',
38 | name: 'menu1-2-1',
39 | meta: { title: 'menu1-2-1' }
40 | },
41 | {
42 | path: 'menu1-2-2',
43 | name: 'menu1-2-2'
44 | }]
45 | }]
46 | }]
47 | }]
48 |
49 | const router = new VueRouter({
50 | routes
51 | })
52 |
53 | describe('Breadcrumb.vue', () => {
54 | const wrapper = mount(Breadcrumb, {
55 | localVue,
56 | router
57 | })
58 | it('dashboard', () => {
59 | router.push('/dashboard')
60 | const len = wrapper.findAll('.el-breadcrumb__inner').length
61 | expect(len).toBe(1)
62 | })
63 | it('normal route', () => {
64 | router.push('/menu/menu1')
65 | const len = wrapper.findAll('.el-breadcrumb__inner').length
66 | expect(len).toBe(2)
67 | })
68 | it('nested route', () => {
69 | router.push('/menu/menu1/menu1-2/menu1-2-1')
70 | const len = wrapper.findAll('.el-breadcrumb__inner').length
71 | expect(len).toBe(4)
72 | })
73 | it('no meta.title', () => {
74 | router.push('/menu/menu1/menu1-2/menu1-2-2')
75 | const len = wrapper.findAll('.el-breadcrumb__inner').length
76 | expect(len).toBe(3)
77 | })
78 | // it('click link', () => {
79 | // router.push('/menu/menu1/menu1-2/menu1-2-2')
80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
81 | // const second = breadcrumbArray.at(1)
82 | // console.log(breadcrumbArray)
83 | // const href = second.find('a').attributes().href
84 | // expect(href).toBe('#/menu/menu1')
85 | // })
86 | // it('noRedirect', () => {
87 | // router.push('/menu/menu1/menu1-2/menu1-2-1')
88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
89 | // const redirectBreadcrumb = breadcrumbArray.at(2)
90 | // expect(redirectBreadcrumb.contains('a')).toBe(false)
91 | // })
92 | it('last breadcrumb', () => {
93 | router.push('/menu/menu1/menu1-2/menu1-2-1')
94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
95 | const redirectBreadcrumb = breadcrumbArray.at(3)
96 | expect(redirectBreadcrumb.contains('a')).toBe(false)
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/vue-admin/src/store/modules/user.js:
--------------------------------------------------------------------------------
1 | import { login, logout, getInfo, refreshToken } from '@/api/user'
2 | import { getToken, setToken, removeToken, getTokenExpire, setTokenExpire, removeTokenExpire } from '@/utils/auth'
3 | import { resetRouter } from '@/router'
4 |
5 | const state = {
6 | token: getToken(),
7 | name: '',
8 | avatar: '',
9 | tokenExpire: getTokenExpire()
10 | }
11 |
12 | const mutations = {
13 | SET_TOKEN: (state, token) => {
14 | state.token = token
15 | },
16 | SET_NAME: (state, name) => {
17 | state.name = name
18 | },
19 | SET_AVATAR: (state, avatar) => {
20 | state.avatar = avatar
21 | },
22 | SET_TOKENEXPIRE: (state, token) => {
23 | state.tokenExpire = token
24 | }
25 | }
26 |
27 | const actions = {
28 | // user login
29 | login({ commit }, userInfo) {
30 | const { username, password } = userInfo
31 | return new Promise((resolve, reject) => {
32 | login({ username: username.trim(), password: password }).then(response => {
33 | const data = response
34 | commit('SET_TOKEN', data.token)
35 | commit('SET_TOKENEXPIRE', data.expire)
36 | setToken(data.token)
37 | setTokenExpire(data.expire)
38 | resolve()
39 | }).catch(error => {
40 | reject(error)
41 | })
42 | })
43 | },
44 |
45 | refreshToken({ commit }) {
46 | return new Promise((resolve, reject) => {
47 | refreshToken().then(response => {
48 | const data = response
49 | commit('SET_TOKEN', data.token)
50 | commit('SET_TOKENEXPIRE', data.expire)
51 | setToken(data.token)
52 | setTokenExpire(data.expire)
53 | resolve()
54 | }).catch(error => {
55 | reject(error)
56 | })
57 | })
58 | },
59 |
60 | // get user info
61 | getInfo({ commit }) {
62 | return new Promise((resolve, reject) => {
63 | getInfo().then(response => {
64 | const data = response.data
65 | if (!data) {
66 | reject('Verification failed, please Login again.')
67 | }
68 | const { Name, Avatar } = data
69 | commit('SET_NAME', Name)
70 | commit('SET_AVATAR', Avatar)
71 | resolve(data)
72 | }).catch(error => {
73 | reject(error)
74 | })
75 | })
76 | },
77 |
78 | // user logout
79 | logout({ commit, state }) {
80 | return new Promise((resolve, reject) => {
81 | logout(state.token).then(() => {
82 | commit('SET_TOKEN', '')
83 | commit('SET_TOKENEXPIRE', '')
84 | removeToken()
85 | removeTokenExpire()
86 | resetRouter()
87 | resolve()
88 | }).catch(error => {
89 | reject(error)
90 | })
91 | })
92 | },
93 |
94 | // remove token
95 | resetToken({ commit }) {
96 | return new Promise(resolve => {
97 | commit('SET_TOKEN', '')
98 | commit('SET_TOKENEXPIRE', '')
99 | removeToken()
100 | removeTokenExpire()
101 | resolve()
102 | })
103 | }
104 | }
105 |
106 | export default {
107 | namespaced: true,
108 | state,
109 | mutations,
110 | actions
111 | }
112 |
113 |
--------------------------------------------------------------------------------
/middleware/myjwt/gin_jwt.go:
--------------------------------------------------------------------------------
1 | package myjwt
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "time"
7 |
8 | jwt "github.com/appleboy/gin-jwt/v2"
9 | "github.com/gin-gonic/gin"
10 |
11 | "gin-vue/models"
12 | "gin-vue/pkg/setting"
13 | )
14 |
15 | var identityKey = setting.IdentityKey
16 |
17 | func GinJWTMiddlewareInit(jwtAuthorizator IAuthorizator) (authMiddleware *jwt.GinJWTMiddleware) {
18 | authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
19 | Realm: "test zone",
20 | Key: []byte("secret key"),
21 | Timeout: time.Minute * 5,
22 | MaxRefresh: time.Hour,
23 | IdentityKey: identityKey,
24 | PayloadFunc: func(data interface{}) jwt.MapClaims {
25 | if v, ok := data.(*models.User); ok {
26 | //get claims from username
27 | v.UserClaims = models.GetUserClaims(v.UserName)
28 | jsonClaim, _ := json.Marshal(v.UserClaims)
29 | //maps the claims in the JWT
30 | return jwt.MapClaims{
31 | "userName": v.UserName,
32 | "userClaims": string(jsonClaim),
33 | }
34 | }
35 | return jwt.MapClaims{}
36 | },
37 | IdentityHandler: func(c *gin.Context) interface{} {
38 | claims := jwt.ExtractClaims(c)
39 | //extracts identity from claims
40 | jsonClaim := claims["userClaims"].(string)
41 | var userClaims []models.Claims
42 | json.Unmarshal([]byte(jsonClaim), &userClaims)
43 | //Set the identity
44 | return &models.User{
45 | UserName: claims["userName"].(string),
46 | UserClaims: userClaims,
47 | }
48 | },
49 | Authenticator: func(c *gin.Context) (interface{}, error) {
50 | //handles the login logic. On success LoginResponse is called, on failure Unauthorized is called
51 | var loginVals models.Auth
52 | if err := c.ShouldBind(&loginVals); err != nil {
53 | return "", jwt.ErrMissingLoginValues
54 | }
55 | userID := loginVals.Username
56 | password := loginVals.Password
57 |
58 | if models.CheckAuth(userID, password) {
59 | return &models.User{
60 | UserName: userID,
61 | }, nil
62 | }
63 |
64 | return nil, jwt.ErrFailedAuthentication
65 | },
66 | //receives identity and handles authorization logic
67 | Authorizator: jwtAuthorizator.HandleAuthorizator,
68 | //handles unauthorized logic
69 | Unauthorized: func(c *gin.Context, code int, message string) {
70 | c.JSON(code, gin.H{
71 | "code": code,
72 | "message": message,
73 | })
74 | },
75 | // TokenLookup is a string in the form of ":" that is used
76 | // to extract token from the request.
77 | // Optional. Default value "header:Authorization".
78 | // Possible values:
79 | // - "header:"
80 | // - "query:"
81 | // - "cookie:"
82 | // - "param:"
83 | TokenLookup: "header: Authorization, query: token, cookie: jwt",
84 | // TokenLookup: "query:token",
85 | // TokenLookup: "cookie:token",
86 |
87 | // TokenHeadName is a string in the header. Default value is "Bearer"
88 | TokenHeadName: "Bearer",
89 |
90 | // TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
91 | TimeFunc: time.Now,
92 | })
93 |
94 | if err != nil {
95 | log.Fatal("JWT Error:" + err.Error())
96 | }
97 | return
98 | }
99 |
--------------------------------------------------------------------------------
/vue-admin/README.md:
--------------------------------------------------------------------------------
1 | # vue-admin-template
2 |
3 | English | [简体中文](./README-zh.md)
4 |
5 | > A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
6 |
7 | **Live demo:** http://panjiachen.github.io/vue-admin-template
8 |
9 |
10 | **The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`**
11 |
12 | ## Build Setup
13 |
14 |
15 | ```bash
16 | # clone the project
17 | git clone https://github.com/PanJiaChen/vue-admin-template.git
18 |
19 | # enter the project directory
20 | cd vue-admin-template
21 |
22 | # install dependency
23 | npm install
24 |
25 | # develop
26 | npm run dev
27 | ```
28 |
29 | This will automatically open http://localhost:9528
30 |
31 | ## Build
32 |
33 | ```bash
34 | # build for test environment
35 | npm run build:stage
36 |
37 | # build for production environment
38 | npm run build:prod
39 | ```
40 |
41 | ## Advanced
42 |
43 | ```bash
44 | # preview the release environment effect
45 | npm run preview
46 |
47 | # preview the release environment effect + static resource analysis
48 | npm run preview -- --report
49 |
50 | # code format check
51 | npm run lint
52 |
53 | # code format check and auto fix
54 | npm run lint -- --fix
55 | ```
56 |
57 | Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
58 |
59 | ## Demo
60 |
61 | 
62 |
63 | ## Extra
64 |
65 | If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
66 |
67 | For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
68 |
69 | ## Related Project
70 |
71 | [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
72 |
73 | [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
74 |
75 | [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
76 |
77 | ## Browsers support
78 |
79 | Modern browsers and Internet Explorer 10+.
80 |
81 | | [
](http://godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari |
82 | | --------- | --------- | --------- | --------- |
83 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
84 |
85 | ## License
86 |
87 | [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
88 |
89 | Copyright (c) 2017-present PanJiaChen
90 |
--------------------------------------------------------------------------------
/vue-admin/README-zh.md:
--------------------------------------------------------------------------------
1 | # vue-admin-template
2 |
3 | > 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
4 |
5 | [线上地址](http://panjiachen.github.io/vue-admin-template)
6 |
7 | [国内访问](https://panjiachen.gitee.io/vue-admin-template)
8 |
9 | 目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。
10 |
11 | ## Extra
12 |
13 | 如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
14 |
15 | ## 相关项目
16 |
17 | [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
18 |
19 | [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
20 |
21 | [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
22 |
23 | 写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
24 |
25 | - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
26 | - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
27 | - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
28 | - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
29 | - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
30 |
31 | ## Build Setup
32 |
33 | ```bash
34 | # 克隆项目
35 | git clone https://github.com/PanJiaChen/vue-admin-template.git
36 |
37 | # 进入项目目录
38 | cd vue-admin-template
39 |
40 | # 安装依赖
41 | npm install
42 |
43 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
44 | npm install --registry=https://registry.npm.taobao.org
45 |
46 | # 启动服务
47 | npm run dev
48 | ```
49 |
50 | 浏览器访问 [http://localhost:9528](http://localhost:9528)
51 |
52 | ## 发布
53 |
54 | ```bash
55 | # 构建测试环境
56 | npm run build:stage
57 |
58 | # 构建生产环境
59 | npm run build:prod
60 | ```
61 |
62 | ## 其它
63 |
64 | ```bash
65 | # 预览发布环境效果
66 | npm run preview
67 |
68 | # 预览发布环境效果 + 静态资源分析
69 | npm run preview -- --report
70 |
71 | # 代码格式检查
72 | npm run lint
73 |
74 | # 代码格式检查并自动修复
75 | npm run lint -- --fix
76 | ```
77 |
78 | 更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
79 |
80 | ## Demo
81 |
82 | 
83 |
84 | ## Browsers support
85 |
86 | Modern browsers and Internet Explorer 10+.
87 |
88 | | [
](http://godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari |
89 | | --------- | --------- | --------- | --------- |
90 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
91 |
92 | ## License
93 |
94 | [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
95 |
96 | Copyright (c) 2017-present PanJiaChen
97 |
--------------------------------------------------------------------------------
/vue-admin/src/icons/svg/qq.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vue-admin/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { MessageBox, Message } from 'element-ui'
3 | import store from '@/store'
4 | import router from '@/router'
5 | import { getToken, getTokenExpire } from '@/utils/auth'
6 |
7 | // create an axios instance
8 | const service = axios.create({
9 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
10 | // withCredentials: true, // send cookies when cross-domain requests
11 | timeout: 5000 // request timeout
12 | })
13 |
14 | // request interceptor
15 | service.interceptors.request.use(
16 | config => {
17 | // do something before request is sent
18 |
19 | if (store.getters.token) {
20 | // let each request carry token
21 | // ['X-Token'] is a custom headers key
22 | // please modify it according to the actual situation
23 | // config.headers['X-Token'] = getToken()
24 | config.headers['Authorization'] = 'Bearer ' + getToken()
25 | }
26 | return config
27 | },
28 | error => {
29 | // do something with request error
30 | console.log(error) // for debug
31 | return Promise.reject(error)
32 | }
33 | )
34 |
35 | // response interceptor
36 | service.interceptors.response.use(
37 | /**
38 | * If you want to get http information such as headers or status
39 | * Please return response => response
40 | */
41 |
42 | /**
43 | * Determine the request status by custom code
44 | * Here is just an example
45 | * You can also judge the status by HTTP Status Code
46 | */
47 | response => {
48 | const res = response.data
49 | // if the custom code is not 200, it is judged as an error.
50 | if (res.code !== 200) {
51 | Message({
52 | message: res.message || 'Error',
53 | type: 'error',
54 | duration: 5 * 1000
55 | })
56 |
57 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
58 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
59 | // to re-login
60 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
61 | confirmButtonText: 'Re-Login',
62 | cancelButtonText: 'Cancel',
63 | type: 'warning'
64 | }).then(() => {
65 | store.dispatch('user/resetToken').then(() => {
66 | location.reload()
67 | })
68 | })
69 | }
70 | return Promise.reject(new Error(res.message || 'Error'))
71 | } else {
72 | return res
73 | }
74 | },
75 | error => {
76 | // if code is 401, refresh the token
77 | if (error.response && error.response.status === 401) {
78 | var curTime = new Date()
79 | var tokenExpire = new Date(getTokenExpire())
80 | var allowTime = new Date(curTime.setMinutes(curTime.getMinutes() + 30))
81 | // if token expire time small than current time add 30 minute, allow to refresh the token
82 | if (tokenExpire < allowTime) {
83 | store.dispatch('user/refreshToken').then(() => {
84 | location.reload()
85 | })
86 | } else {
87 | store.dispatch('user/resetToken').then(() => {
88 | location.reload()
89 | })
90 | }
91 | } else if (error.response && error.response.status === 403) {
92 | router.push({ path: '/403' })
93 | } else {
94 | console.log('err' + error) // for debug
95 | Message({
96 | message: error.message,
97 | type: 'error',
98 | duration: 5 * 1000
99 | })
100 | return Promise.reject(error)
101 | }
102 | }
103 | )
104 |
105 | export default service
106 |
--------------------------------------------------------------------------------
/routers/api/v1/tag.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "gin-vue/models"
5 | "gin-vue/pkg/e"
6 | "gin-vue/pkg/setting"
7 | "gin-vue/pkg/util"
8 | "net/http"
9 |
10 | "github.com/astaxie/beego/validation"
11 | "github.com/gin-gonic/gin"
12 | "github.com/unknwon/com"
13 | )
14 |
15 | //获取多个文章标签
16 | func GetTags(c *gin.Context) {
17 | name := c.Query("name")
18 | maps := make(map[string]interface{})
19 | data := make(map[string]interface{})
20 | if name != "" {
21 | maps["name"] = name
22 | }
23 |
24 | var state int = -1
25 | if arg := c.Query("state"); arg != "" {
26 | state = com.StrTo(arg).MustInt()
27 | maps["state"] = state
28 | }
29 | code := e.SUCCESS
30 |
31 | data["lists"] = models.GetTags(util.GetPage(c), setting.PageSize, maps)
32 | data["total"] = models.GetTagTotal(maps)
33 |
34 | c.JSON(http.StatusOK, gin.H{
35 | "code": code,
36 | "msg": e.GetMsg(code),
37 | "data": data,
38 | })
39 |
40 | }
41 |
42 | //新增文章标签
43 | func AddTag(c *gin.Context) {
44 | name := c.Query("name")
45 | state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()
46 | createdBy := c.Query("created_by")
47 |
48 | valid := validation.Validation{}
49 | valid.Required(name, "name").Message("名称不能为空")
50 | valid.MaxSize(name, 100, "name").Message("名称最长为100字符")
51 | valid.Required(createdBy, "created_by").Message("创建人不能为空")
52 | valid.MaxSize(createdBy, 100, "created_by").Message("创建人最长为100字符")
53 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
54 |
55 | code := e.INVALID_PARAMS
56 | if !valid.HasErrors() {
57 | if !models.ExistTagByName(name) {
58 | code = e.SUCCESS
59 | models.AddTag(name, state, createdBy)
60 | } else {
61 | code = e.ERROR_EXIST_TAG
62 | }
63 | }
64 |
65 | c.JSON(http.StatusOK, gin.H{
66 | "code": code,
67 | "msg": e.GetMsg(code),
68 | "data": make(map[string]string),
69 | })
70 |
71 | }
72 |
73 | //修改文章标签
74 | func EditTag(c *gin.Context) {
75 | id := com.StrTo(c.Param("id")).MustInt()
76 | name := c.Query("name")
77 | modifiedBy := c.Query("modified_by")
78 |
79 | valid := validation.Validation{}
80 |
81 | var state int = -1
82 | if arg := c.Query("state"); arg != "" {
83 | state = com.StrTo(arg).MustInt()
84 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
85 | }
86 |
87 | valid.Required(id, "id").Message("ID不能为空")
88 | valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
89 | valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
90 | valid.MaxSize(name, 100, "name").Message("名称最长为100字符")
91 |
92 | code := e.INVALID_PARAMS
93 | if !valid.HasErrors() {
94 | code = e.SUCCESS
95 | if models.ExistTagByID(id) {
96 | data := make(map[string]interface{})
97 | data["modified_by"] = modifiedBy
98 | if name != "" {
99 | data["name"] = name
100 | }
101 | if state != -1 {
102 | data["state"] = state
103 | }
104 |
105 | models.EditTag(id, data)
106 | } else {
107 | code = e.ERROR_NOT_EXIST_TAG
108 | }
109 | }
110 |
111 | c.JSON(http.StatusOK, gin.H{
112 | "code": code,
113 | "msg": e.GetMsg(code),
114 | "data": make(map[string]string),
115 | })
116 | }
117 |
118 | //删除文章标签
119 | func DeleteTag(c *gin.Context) {
120 | id := com.StrTo(c.Param("id")).MustInt()
121 |
122 | valid := validation.Validation{}
123 | valid.Min(id, 1, "id").Message("ID必须大于0")
124 |
125 | code := e.INVALID_PARAMS
126 | if !valid.HasErrors() {
127 | code = e.SUCCESS
128 | if models.ExistTagByID(id) {
129 | models.DeleteTag(id)
130 | } else {
131 | code = e.ERROR_NOT_EXIST_TAG
132 | }
133 | }
134 |
135 | c.JSON(http.StatusOK, gin.H{
136 | "code": code,
137 | "msg": e.GetMsg(code),
138 | "data": make(map[string]string),
139 | })
140 | }
141 |
--------------------------------------------------------------------------------
/vue-admin/src/layout/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
31 |
32 |
33 |
34 |
61 |
62 |
140 |
--------------------------------------------------------------------------------
/vue-admin/src/styles/sidebar.scss:
--------------------------------------------------------------------------------
1 | #app {
2 |
3 | .main-container {
4 | min-height: 100%;
5 | transition: margin-left .28s;
6 | margin-left: $sideBarWidth;
7 | position: relative;
8 | }
9 |
10 | .sidebar-container {
11 | transition: width 0.28s;
12 | width: $sideBarWidth !important;
13 | background-color: $menuBg;
14 | height: 100%;
15 | position: fixed;
16 | font-size: 0px;
17 | top: 0;
18 | bottom: 0;
19 | left: 0;
20 | z-index: 1001;
21 | overflow: hidden;
22 |
23 | // reset element-ui css
24 | .horizontal-collapse-transition {
25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
26 | }
27 |
28 | .scrollbar-wrapper {
29 | overflow-x: hidden !important;
30 | }
31 |
32 | .el-scrollbar__bar.is-vertical {
33 | right: 0px;
34 | }
35 |
36 | .el-scrollbar {
37 | height: 100%;
38 | }
39 |
40 | &.has-logo {
41 | .el-scrollbar {
42 | height: calc(100% - 50px);
43 | }
44 | }
45 |
46 | .is-horizontal {
47 | display: none;
48 | }
49 |
50 | a {
51 | display: inline-block;
52 | width: 100%;
53 | overflow: hidden;
54 | }
55 |
56 | .svg-icon {
57 | margin-right: 16px;
58 | }
59 |
60 | .el-menu {
61 | border: none;
62 | height: 100%;
63 | width: 100% !important;
64 | }
65 |
66 | // menu hover
67 | .submenu-title-noDropdown,
68 | .el-submenu__title {
69 | &:hover {
70 | background-color: $menuHover !important;
71 | }
72 | }
73 |
74 | .is-active>.el-submenu__title {
75 | color: $subMenuActiveText !important;
76 | }
77 |
78 | & .nest-menu .el-submenu>.el-submenu__title,
79 | & .el-submenu .el-menu-item {
80 | min-width: $sideBarWidth !important;
81 | background-color: $subMenuBg !important;
82 |
83 | &:hover {
84 | background-color: $subMenuHover !important;
85 | }
86 | }
87 | }
88 |
89 | .hideSidebar {
90 | .sidebar-container {
91 | width: 54px !important;
92 | }
93 |
94 | .main-container {
95 | margin-left: 54px;
96 | }
97 |
98 | .submenu-title-noDropdown {
99 | padding: 0 !important;
100 | position: relative;
101 |
102 | .el-tooltip {
103 | padding: 0 !important;
104 |
105 | .svg-icon {
106 | margin-left: 20px;
107 | }
108 | }
109 | }
110 |
111 | .el-submenu {
112 | overflow: hidden;
113 |
114 | &>.el-submenu__title {
115 | padding: 0 !important;
116 |
117 | .svg-icon {
118 | margin-left: 20px;
119 | }
120 |
121 | .el-submenu__icon-arrow {
122 | display: none;
123 | }
124 | }
125 | }
126 |
127 | .el-menu--collapse {
128 | .el-submenu {
129 | &>.el-submenu__title {
130 | &>span {
131 | height: 0;
132 | width: 0;
133 | overflow: hidden;
134 | visibility: hidden;
135 | display: inline-block;
136 | }
137 | }
138 | }
139 | }
140 | }
141 |
142 | .el-menu--collapse .el-menu .el-submenu {
143 | min-width: $sideBarWidth !important;
144 | }
145 |
146 | // mobile responsive
147 | .mobile {
148 | .main-container {
149 | margin-left: 0px;
150 | }
151 |
152 | .sidebar-container {
153 | transition: transform .28s;
154 | width: $sideBarWidth !important;
155 | }
156 |
157 | &.hideSidebar {
158 | .sidebar-container {
159 | pointer-events: none;
160 | transition-duration: 0.3s;
161 | transform: translate3d(-$sideBarWidth, 0, 0);
162 | }
163 | }
164 | }
165 |
166 | .withoutAnimation {
167 |
168 | .main-container,
169 | .sidebar-container {
170 | transition: none;
171 | }
172 | }
173 | }
174 |
175 | // when menu collapsed
176 | .el-menu--vertical {
177 | &>.el-menu {
178 | .svg-icon {
179 | margin-right: 16px;
180 | }
181 | }
182 |
183 | .nest-menu .el-submenu>.el-submenu__title,
184 | .el-menu-item {
185 | &:hover {
186 | // you can use $subMenuHover
187 | background-color: $menuHover !important;
188 | }
189 | }
190 |
191 | // the scroll bar appears when the subMenu is too long
192 | >.el-menu--popup {
193 | max-height: 100vh;
194 | overflow-y: auto;
195 |
196 | &::-webkit-scrollbar-track-piece {
197 | background: #d3dce6;
198 | }
199 |
200 | &::-webkit-scrollbar {
201 | width: 6px;
202 | }
203 |
204 | &::-webkit-scrollbar-thumb {
205 | background: #99a9bf;
206 | border-radius: 20px;
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/vue-admin/vue.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const defaultSettings = require('./src/settings.js')
4 |
5 | function resolve(dir) {
6 | return path.join(__dirname, dir)
7 | }
8 |
9 | const name = defaultSettings.title || 'vue Admin Template' // page title
10 |
11 | // If your port is set to 80,
12 | // use administrator privileges to execute the command line.
13 | // For example, Mac: sudo npm run
14 | // You can change the port by the following methods:
15 | // port = 9528 npm run dev OR npm run dev --port = 9528
16 | const port = process.env.port || process.env.npm_config_port || 9528 // dev port
17 |
18 | // All configuration item explanations can be find in https://cli.vuejs.org/config/
19 | module.exports = {
20 | /**
21 | * You will need to set publicPath if you plan to deploy your site under a sub path,
22 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
23 | * then publicPath should be set to "/bar/".
24 | * In most cases please use '/' !!!
25 | * Detail: https://cli.vuejs.org/config/#publicpath
26 | */
27 | publicPath: '/',
28 | outputDir: 'dist',
29 | assetsDir: 'static',
30 | lintOnSave: process.env.NODE_ENV === 'development',
31 | productionSourceMap: false,
32 | devServer: {
33 | port: port,
34 | open: true,
35 | overlay: {
36 | warnings: false,
37 | errors: true
38 | },
39 | proxy: {
40 | // change xxx-api/login => mock/login
41 | // detail: https://cli.vuejs.org/config/#devserver-proxy
42 | [process.env.VUE_APP_BASE_API]: {
43 | target: `http://127.0.0.1:${port}/mock`,
44 | changeOrigin: true,
45 | pathRewrite: {
46 | ['^' + process.env.VUE_APP_BASE_API]: ''
47 | }
48 | }
49 | },
50 | after: require('./mock/mock-server.js')
51 | },
52 | configureWebpack: {
53 | // provide the app's title in webpack's name field, so that
54 | // it can be accessed in index.html to inject the correct title.
55 | name: name,
56 | resolve: {
57 | alias: {
58 | '@': resolve('src')
59 | }
60 | }
61 | },
62 | chainWebpack(config) {
63 | config.plugins.delete('preload') // TODO: need test
64 | config.plugins.delete('prefetch') // TODO: need test
65 |
66 | // set svg-sprite-loader
67 | config.module
68 | .rule('svg')
69 | .exclude.add(resolve('src/icons'))
70 | .end()
71 | config.module
72 | .rule('icons')
73 | .test(/\.svg$/)
74 | .include.add(resolve('src/icons'))
75 | .end()
76 | .use('svg-sprite-loader')
77 | .loader('svg-sprite-loader')
78 | .options({
79 | symbolId: 'icon-[name]'
80 | })
81 | .end()
82 |
83 | // set preserveWhitespace
84 | config.module
85 | .rule('vue')
86 | .use('vue-loader')
87 | .loader('vue-loader')
88 | .tap(options => {
89 | options.compilerOptions.preserveWhitespace = true
90 | return options
91 | })
92 | .end()
93 |
94 | config
95 | // https://webpack.js.org/configuration/devtool/#development
96 | .when(process.env.NODE_ENV === 'development',
97 | config => config.devtool('cheap-source-map')
98 | )
99 |
100 | config
101 | .when(process.env.NODE_ENV !== 'development',
102 | config => {
103 | config
104 | .plugin('ScriptExtHtmlWebpackPlugin')
105 | .after('html')
106 | .use('script-ext-html-webpack-plugin', [{
107 | // `runtime` must same as runtimeChunk name. default is `runtime`
108 | inline: /runtime\..*\.js$/
109 | }])
110 | .end()
111 | config
112 | .optimization.splitChunks({
113 | chunks: 'all',
114 | cacheGroups: {
115 | libs: {
116 | name: 'chunk-libs',
117 | test: /[\\/]node_modules[\\/]/,
118 | priority: 10,
119 | chunks: 'initial' // only package third parties that are initially dependent
120 | },
121 | elementUI: {
122 | name: 'chunk-elementUI', // split elementUI into a single package
123 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
124 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
125 | },
126 | commons: {
127 | name: 'chunk-commons',
128 | test: resolve('src/components'), // can customize your rules
129 | minChunks: 3, // minimum common number
130 | priority: 5,
131 | reuseExistingChunk: true
132 | }
133 | }
134 | })
135 | config.optimization.runtimeChunk('single')
136 | }
137 | )
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/vue-admin/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | /* Layout */
7 | import Layout from '@/layout'
8 |
9 | /**
10 | * Note: sub-menu only appear when route children.length >= 1
11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
12 | *
13 | * hidden: true if set true, item will not show in the sidebar(default is false)
14 | * alwaysShow: true if set true, will always show the root menu
15 | * if not set alwaysShow, when item has more than one children route,
16 | * it will becomes nested mode, otherwise not show the root menu
17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
18 | * name:'router-name' the name is used by (must set!!!)
19 | * meta : {
20 | roles: ['admin','editor'] control the page roles (you can set multiple roles)
21 | title: 'title' the name show in sidebar and breadcrumb (recommend set)
22 | icon: 'svg-name' the icon show in the sidebar
23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
25 | }
26 | */
27 |
28 | /**
29 | * constantRoutes
30 | * a base page that does not have permission requirements
31 | * all roles can be accessed
32 | */
33 | export const constantRoutes = [
34 | {
35 | path: '/login',
36 | component: () => import('@/views/login/index'),
37 | hidden: true
38 | },
39 |
40 | {
41 | path: '/404',
42 | component: () => import('@/views/404'),
43 | hidden: true
44 | },
45 |
46 | {
47 | path: '/403',
48 | component: () => import('@/views/403'),
49 | hidden: true
50 | },
51 |
52 | {
53 | path: '/',
54 | component: Layout,
55 | redirect: '/dashboard',
56 | children: [{
57 | path: 'dashboard',
58 | name: 'Dashboard',
59 | component: () => import('@/views/dashboard/index'),
60 | meta: { title: 'Dashboard', icon: 'dashboard' }
61 | }]
62 | },
63 |
64 | {
65 | path: '/example',
66 | component: Layout,
67 | redirect: '/example/table',
68 | name: 'Example',
69 | meta: { title: 'Example', icon: 'example' },
70 | children: [
71 | {
72 | path: 'table',
73 | name: 'Table',
74 | component: () => import('@/views/table/index'),
75 | meta: { title: 'Table', icon: 'table' }
76 | },
77 | {
78 | path: 'tree',
79 | name: 'Tree',
80 | component: () => import('@/views/tree/index'),
81 | meta: { title: 'Tree', icon: 'tree' }
82 | }
83 | ]
84 | },
85 |
86 | {
87 | path: '/form',
88 | component: Layout,
89 | children: [
90 | {
91 | path: 'index',
92 | name: 'Form',
93 | component: () => import('@/views/form/index'),
94 | meta: { title: 'Form', icon: 'form' }
95 | }
96 | ]
97 | },
98 |
99 | {
100 | path: '/nested',
101 | component: Layout,
102 | redirect: '/nested/menu1',
103 | name: 'Nested',
104 | meta: {
105 | title: 'Nested',
106 | icon: 'nested'
107 | },
108 | children: [
109 | {
110 | path: 'menu1',
111 | component: () => import('@/views/nested/menu1/index'), // Parent router-view
112 | name: 'Menu1',
113 | meta: { title: 'Menu1' },
114 | children: [
115 | {
116 | path: 'menu1-1',
117 | component: () => import('@/views/nested/menu1/menu1-1'),
118 | name: 'Menu1-1',
119 | meta: { title: 'Menu1-1' }
120 | },
121 | {
122 | path: 'menu1-2',
123 | component: () => import('@/views/nested/menu1/menu1-2'),
124 | name: 'Menu1-2',
125 | meta: { title: 'Menu1-2' },
126 | children: [
127 | {
128 | path: 'menu1-2-1',
129 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
130 | name: 'Menu1-2-1',
131 | meta: { title: 'Menu1-2-1' }
132 | },
133 | {
134 | path: 'menu1-2-2',
135 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
136 | name: 'Menu1-2-2',
137 | meta: { title: 'Menu1-2-2' }
138 | }
139 | ]
140 | },
141 | {
142 | path: 'menu1-3',
143 | component: () => import('@/views/nested/menu1/menu1-3'),
144 | name: 'Menu1-3',
145 | meta: { title: 'Menu1-3' }
146 | }
147 | ]
148 | },
149 | {
150 | path: 'menu2',
151 | component: () => import('@/views/nested/menu2/index'),
152 | meta: { title: 'menu2' }
153 | }
154 | ]
155 | },
156 |
157 | {
158 | path: 'external-link',
159 | component: Layout,
160 | children: [
161 | {
162 | path: 'https://panjiachen.github.io/vue-element-admin-site/#/',
163 | meta: { title: 'External Link', icon: 'link' }
164 | }
165 | ]
166 | },
167 |
168 | // 404 page must be placed at the end !!!
169 | { path: '*', redirect: '/404', hidden: true }
170 | ]
171 |
172 | const createRouter = () => new Router({
173 | // mode: 'history', // require service support
174 | scrollBehavior: () => ({ y: 0 }),
175 | routes: constantRoutes
176 | })
177 |
178 | const router = createRouter()
179 |
180 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
181 | export function resetRouter() {
182 | const newRouter = createRouter()
183 | router.matcher = newRouter.matcher // reset router
184 | }
185 |
186 | export default router
187 |
--------------------------------------------------------------------------------
/vue-admin/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parserOptions: {
4 | parser: 'babel-eslint',
5 | sourceType: 'module'
6 | },
7 | env: {
8 | browser: true,
9 | node: true,
10 | es6: true,
11 | },
12 | extends: ['plugin:vue/recommended', 'eslint:recommended'],
13 |
14 | // add your custom rules here
15 | //it is base on https://github.com/vuejs/eslint-config-vue
16 | rules: {
17 | "vue/max-attributes-per-line": [2, {
18 | "singleline": 10,
19 | "multiline": {
20 | "max": 1,
21 | "allowFirstLine": false
22 | }
23 | }],
24 | "vue/singleline-html-element-content-newline": "off",
25 | "vue/multiline-html-element-content-newline":"off",
26 | "vue/name-property-casing": ["error", "PascalCase"],
27 | "vue/no-v-html": "off",
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': ["error", "always", {"null": "ignore"}],
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': 0,
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 |
--------------------------------------------------------------------------------
/vue-admin/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
登 录
7 |
8 |
9 |
10 |
11 |
12 |
13 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
39 |
40 |
41 |
42 |
43 |
44 | Login
45 |
46 |
47 | username: admin
48 | password: 111111
49 |
50 |
51 |
52 |
53 |
54 |
55 |
117 |
118 |
164 |
165 |
228 |
--------------------------------------------------------------------------------
/routers/api/v1/article.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "gin-vue/pkg/e"
5 | "gin-vue/pkg/setting"
6 | "gin-vue/pkg/util"
7 | "log"
8 | "net/http"
9 |
10 | "github.com/astaxie/beego/validation"
11 | "github.com/gin-gonic/gin"
12 | "github.com/unknwon/com"
13 |
14 | "gin-vue/models"
15 | )
16 |
17 | //获取单个文章
18 | func GetArticle(c *gin.Context) {
19 | id := com.StrTo(c.Param("id")).MustInt()
20 |
21 | valid := validation.Validation{}
22 | valid.Min(id, 1, "id").Message("ID必须大于0")
23 |
24 | code := e.INVALID_PARAMS
25 | var data interface{}
26 | if !valid.HasErrors() {
27 | if models.ExistArticleByID(id) {
28 | data = models.GetArticle(id)
29 | code = e.SUCCESS
30 | } else {
31 | code = e.ERROR_NOT_EXIST_ARTICLE
32 | }
33 | } else {
34 | for _, err := range valid.Errors {
35 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
36 | }
37 | }
38 |
39 | c.JSON(http.StatusOK, gin.H{
40 | "code": code,
41 | "msg": e.GetMsg(code),
42 | "data": data,
43 | })
44 | }
45 |
46 | //获取多个文章
47 | func GetArticles(c *gin.Context) {
48 | data := make(map[string]interface{})
49 | maps := make(map[string]interface{})
50 | valid := validation.Validation{}
51 |
52 | var state int = -1
53 | if arg := c.Query("state"); arg != "" {
54 | state = com.StrTo(arg).MustInt()
55 | maps["state"] = state
56 |
57 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
58 | }
59 |
60 | var tagId int = -1
61 | if arg := c.Query("tag_id"); arg != "" {
62 | tagId = com.StrTo(arg).MustInt()
63 | maps["tag_id"] = tagId
64 |
65 | valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
66 | }
67 |
68 | code := e.INVALID_PARAMS
69 | if !valid.HasErrors() {
70 | code = e.SUCCESS
71 |
72 | data["lists"] = models.GetArticles(util.GetPage(c), setting.PageSize, maps)
73 | data["total"] = models.GetArticleTotal(maps)
74 |
75 | } else {
76 | for _, err := range valid.Errors {
77 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
78 | }
79 | }
80 |
81 | c.JSON(http.StatusOK, gin.H{
82 | "code": code,
83 | "msg": e.GetMsg(code),
84 | "data": data,
85 | })
86 | }
87 |
88 | //新增文章
89 | func AddArticle(c *gin.Context) {
90 | tagId := com.StrTo(c.Query("tag_id")).MustInt()
91 | title := c.Query("title")
92 | desc := c.Query("desc")
93 | content := c.Query("content")
94 | createdBy := c.Query("created_by")
95 | state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()
96 |
97 | valid := validation.Validation{}
98 | valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
99 | valid.Required(title, "title").Message("标题不能为空")
100 | valid.Required(desc, "desc").Message("简述不能为空")
101 | valid.Required(content, "content").Message("内容不能为空")
102 | valid.Required(createdBy, "created_by").Message("创建人不能为空")
103 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
104 |
105 | code := e.INVALID_PARAMS
106 | if !valid.HasErrors() {
107 | if models.ExistTagByID(tagId) {
108 | data := make(map[string]interface{})
109 | data["tag_id"] = tagId
110 | data["title"] = title
111 | data["desc"] = desc
112 | data["content"] = content
113 | data["created_by"] = createdBy
114 | data["state"] = state
115 |
116 | models.AddArticle(data)
117 | code = e.SUCCESS
118 | } else {
119 | code = e.ERROR_NOT_EXIST_TAG
120 | }
121 | } else {
122 | for _, err := range valid.Errors {
123 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
124 | }
125 | }
126 |
127 | c.JSON(http.StatusOK, gin.H{
128 | "code": code,
129 | "msg": e.GetMsg(code),
130 | "data": make(map[string]interface{}),
131 | })
132 | }
133 |
134 | //修改文章
135 | func EditArticle(c *gin.Context) {
136 | valid := validation.Validation{}
137 |
138 | id := com.StrTo(c.Param("id")).MustInt()
139 | tagId := com.StrTo(c.Query("tag_id")).MustInt()
140 | title := c.Query("title")
141 | desc := c.Query("desc")
142 | content := c.Query("content")
143 | modifiedBy := c.Query("modified_by")
144 |
145 | var state int = -1
146 | if arg := c.Query("state"); arg != "" {
147 | state = com.StrTo(arg).MustInt()
148 | valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
149 | }
150 |
151 | valid.Min(id, 1, "id").Message("ID必须大于0")
152 | valid.MaxSize(title, 100, "title").Message("标题最长为100字符")
153 | valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符")
154 | valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符")
155 | valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
156 | valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
157 |
158 | code := e.INVALID_PARAMS
159 | if !valid.HasErrors() {
160 | if models.ExistArticleByID(id) {
161 | if models.ExistTagByID(tagId) {
162 | data := make(map[string]interface{})
163 | if tagId > 0 {
164 | data["tag_id"] = tagId
165 | }
166 | if title != "" {
167 | data["title"] = title
168 | }
169 | if desc != "" {
170 | data["desc"] = desc
171 | }
172 | if content != "" {
173 | data["content"] = content
174 | }
175 |
176 | data["modified_by"] = modifiedBy
177 |
178 | models.EditArticle(id, data)
179 | code = e.SUCCESS
180 | } else {
181 | code = e.ERROR_NOT_EXIST_TAG
182 | }
183 | } else {
184 | code = e.ERROR_NOT_EXIST_ARTICLE
185 | }
186 | } else {
187 | for _, err := range valid.Errors {
188 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
189 | }
190 | }
191 |
192 | c.JSON(http.StatusOK, gin.H{
193 | "code": code,
194 | "msg": e.GetMsg(code),
195 | "data": make(map[string]string),
196 | })
197 | }
198 |
199 | //删除文章
200 | func DeleteArticle(c *gin.Context) {
201 | id := com.StrTo(c.Param("id")).MustInt()
202 |
203 | valid := validation.Validation{}
204 | valid.Min(id, 1, "id").Message("ID必须大于0")
205 |
206 | code := e.INVALID_PARAMS
207 | if !valid.HasErrors() {
208 | if models.ExistArticleByID(id) {
209 | models.DeleteArticle(id)
210 | code = e.SUCCESS
211 | } else {
212 | code = e.ERROR_NOT_EXIST_ARTICLE
213 | }
214 | } else {
215 | for _, err := range valid.Errors {
216 | log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
217 | }
218 | }
219 |
220 | c.JSON(http.StatusOK, gin.H{
221 | "code": code,
222 | "msg": e.GetMsg(code),
223 | "data": make(map[string]string),
224 | })
225 | }
226 |
--------------------------------------------------------------------------------
/vue-admin/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
OOPS!
12 |
15 |
{{ message }}
16 |
Please check that the URL you entered is correct, or click the button below to return to the homepage.
17 |
Back to home
18 |
19 |
20 |
21 |
22 |
23 |
34 |
35 |
229 |
--------------------------------------------------------------------------------
/docs/sql/mysql.sql:
--------------------------------------------------------------------------------
1 | drop database if exists `blog`;
2 | create database `blog` default character set utf8mb4 collate utf8mb4_unicode_ci;
3 |
4 | use `blog`;
5 |
6 | SET NAMES utf8mb4;
7 | SET FOREIGN_KEY_CHECKS=0;
8 |
9 | -- ----------------------------
10 | -- Table structure for blog_article
11 | -- ----------------------------
12 | DROP TABLE IF EXISTS `blog_article`;
13 | CREATE TABLE `blog_article` (
14 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
15 | `tag_id` int(10) unsigned DEFAULT '0' COMMENT '标签ID',
16 | `title` varchar(100) DEFAULT '' COMMENT '文章标题',
17 | `desc` varchar(255) DEFAULT '' COMMENT '简述',
18 | `content` text COMMENT '内容',
19 | `cover_image_url` varchar(255) DEFAULT '' COMMENT '封面图片地址',
20 | `created_on` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
21 | `created_by` varchar(100) DEFAULT '' COMMENT '创建人',
22 | `modified_on` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
23 | `modified_by` varchar(255) DEFAULT '' COMMENT '修改人',
24 | `deleted_on` int(10) unsigned DEFAULT '0' COMMENT '删除时间',
25 | `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为草稿、1为已发布、2为删除',
26 | PRIMARY KEY (`id`)
27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章管理';
28 |
29 | -- ----------------------------
30 | -- Table structure for blog_auth
31 | -- ----------------------------
32 | DROP TABLE IF EXISTS `blog_auth`;
33 | CREATE TABLE `blog_auth` (
34 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
35 | `username` varchar(50) DEFAULT '' COMMENT '账号',
36 | `password` varchar(50) DEFAULT '' COMMENT '密码',
37 | `avatar` varchar(255) DEFAULT '' COMMENT '头像地址',
38 | PRIMARY KEY (`id`)
39 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
40 |
41 | -- ----------------------------
42 | -- Table structure for blog_tag
43 | -- ----------------------------
44 | DROP TABLE IF EXISTS `blog_tag`;
45 | CREATE TABLE `blog_tag` (
46 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
47 | `name` varchar(100) DEFAULT '' COMMENT '标签名称',
48 | `created_on` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
49 | `created_by` varchar(100) DEFAULT '' COMMENT '创建人',
50 | `modified_on` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
51 | `modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
52 | `deleted_on` int(10) unsigned DEFAULT '0' COMMENT '删除时间',
53 | `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用',
54 | PRIMARY KEY (`id`)
55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章标签管理';
56 |
57 |
58 | -- ----------------------------
59 | -- Table structure for blog_claims
60 | -- ----------------------------
61 | DROP TABLE IF EXISTS `blog_claims`;
62 | CREATE TABLE `blog_claims` (
63 | `claim_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
64 | `auth_id` int(10) unsigned NOT NULL COMMENT '用户ID',
65 | `type` varchar(50) DEFAULT '' COMMENT 'claim类型',
66 | `value` varchar(50) DEFAULT '' COMMENT 'claim值',
67 | PRIMARY KEY (`claim_id`)
68 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
69 |
70 | INSERT INTO `blog_auth`(`id`, `username`, `password`, `avatar`) VALUES (1, 'admin', '111111', 'https://zbj-bucket1.oss-cn-shenzhen.aliyuncs.com/avatar.JPG');
71 | INSERT INTO `blog_auth`(`id`, `username`, `password`, `avatar`) VALUES (2, 'test', '111111', 'https://zbj-bucket1.oss-cn-shenzhen.aliyuncs.com/avatar.JPG');
72 |
73 | INSERT INTO `blog_tag`(`id`, `name`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (1, '1', '2019-08-18 18:56:01', 'test', NULL, '', 0, 1);
74 | INSERT INTO `blog_tag`(`id`, `name`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (2, '2', '2019-08-16 18:56:06', 'test', NULL, '', 0, 1);
75 | INSERT INTO `blog_tag`(`id`, `name`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (3, '3', '2019-08-18 18:56:09', 'test', NULL, '', 0, 1);
76 |
77 | INSERT INTO `blog_article`(`id`, `tag_id`, `title`, `desc`, `content`, `cover_image_url`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (1, 1, 'test1', 'test-desc', 'test-content', '', '2019-08-19 21:00:39', 'test-created', '2019-08-19 21:00:39', '', 0, 0);
78 | INSERT INTO `blog_article`(`id`, `tag_id`, `title`, `desc`, `content`, `cover_image_url`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (2, 1, 'test2', 'test-desc', 'test-content', '', '2019-08-19 21:00:48', 'test-created', '2019-08-19 21:00:48', '', 0, 2);
79 | INSERT INTO `blog_article`(`id`, `tag_id`, `title`, `desc`, `content`, `cover_image_url`, `created_on`, `created_by`, `modified_on`, `modified_by`, `deleted_on`, `state`) VALUES (3, 1, 'test3', 'test-desc', 'test-content', '', '2019-08-19 21:00:49', 'test-created', '2019-08-19 21:00:49', '', 0, 1);
80 |
81 | INSERT INTO `blog_claims`(`claim_id`, `auth_id`, `type`, `value`) VALUES (1, 1, 'role', 'admin');
82 | INSERT INTO `blog_claims`(`claim_id`, `auth_id`, `type`, `value`) VALUES (2, 1, 'role', 'test');
83 | INSERT INTO `blog_claims`(`claim_id`, `auth_id`, `type`, `value`) VALUES (3, 2, 'role', 'test');
84 |
85 |
86 | -- -- ----------------------------
87 | -- -- comdb:Table structure for com_system
88 | -- -- ----------------------------
89 | -- DROP TABLE IF EXISTS `com_system`;
90 | -- CREATE TABLE `com_system` (
91 | -- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
92 | -- `sys_name` varchar(50) NOT NULL DEFAULT '' COMMENT '系统名称',
93 | -- `sys_password` varchar(50) NOT NULL DEFAULT '' COMMENT '系统密码',
94 | -- `email` varchar(50) NOT NULL DEFAULT '' COMMENT '邮箱地址',
95 | -- `state` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '状态 0为禁用、1为启用',
96 | -- PRIMARY KEY (`id`)
97 | -- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
98 |
99 | -- -- ----------------------------
100 | -- -- comdb_sysid:Table structure for sysid_comment
101 | -- -- ----------------------------
102 | -- DROP TABLE IF EXISTS `sysid_comment`;
103 | -- CREATE TABLE `sysid_comment` (
104 | -- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
105 | -- `target_id` varchar(50) NOT NULL DEFAULT '' COMMENT '评论主题的id,可根据需要修改为article_id、course_id等等',
106 | -- `com_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '被评论的评论id,主评论为0',
107 | -- `user_id` varchar(50) NOT NULL DEFAULT '' COMMENT '发表评论的用户id',
108 | -- `user_name` varchar(50) NOT NULL DEFAULT '' COMMENT '发表评论的用户名称(冗余设计)',
109 | -- `avatar_url` varchar(255) NOT NULL DEFAULT '' COMMENT '发表评论的用户头像(冗余设计)',
110 | -- `reply_name` varchar(50) NOT NULL DEFAULT '' COMMENT '回复人的名称',
111 | -- `content` varchar(800) NOT NULL DEFAULT '' COMMENT '评论内容',
112 | -- `created_on` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
113 | -- `support` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '点赞数(出于善良,不设计反对数)',
114 | -- `satisfaction` int(10) NOT NULL DEFAULT '0' COMMENT '满意度(为0时,忽略满意度)',
115 | -- `photo` varchar(800) NOT NULL DEFAULT '' COMMENT '图片地址',
116 | -- PRIMARY KEY (`id`)
117 | -- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
118 |
119 | -- -- ----------------------------
120 | -- -- comdb_sysid:Table structure for sysid_comment
121 | -- -- ----------------------------
122 | -- DROP TABLE IF EXISTS `sysid_support`;
123 | -- CREATE TABLE `sysid_support` (
124 | -- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
125 | -- `com_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '被点赞的评论id',
126 | -- `user_id` varchar(50) NOT NULL DEFAULT '' COMMENT '点赞的用户ID',
127 | -- PRIMARY KEY (`id`)
128 | -- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2 | github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
5 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
7 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
8 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
9 | github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
10 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
11 | github.com/appleboy/gin-jwt v1.0.1 h1:cbLi6ol8KhWUoCBvLFCGpssiBB2iM9eXWwmI9SDG+W0=
12 | github.com/appleboy/gin-jwt v2.5.0+incompatible h1:oLQTP1fiGDoDKoC2UDqXD9iqCP44ABIZMMenfH/xCqw=
13 | github.com/appleboy/gin-jwt v2.5.0+incompatible/go.mod h1:pG7tv32IEe5wEh1NSQzcyD02ZZAqZWp07RdGiIhgaRQ=
14 | github.com/appleboy/gin-jwt/v2 v2.6.4 h1:4YlMh3AjCFnuIRiL27b7TXns7nLx8tU/TiSgh40RRUI=
15 | github.com/appleboy/gin-jwt/v2 v2.6.4/go.mod h1:CZpq1cRw+kqi0+yD2CwVw7VGXrrx4AqBdeZnwxVmoAs=
16 | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
17 | github.com/astaxie/beego v1.12.2 h1:CajUexhSX5ONWDiSCpeQBNVfTzOtPb9e9d+3vuU5FuU=
18 | github.com/astaxie/beego v1.12.2/go.mod h1:TMcqhsbhN3UFpN+RCfysaxPAbrhox6QSS3NIAEp/uzE=
19 | github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
20 | github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
21 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
22 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
23 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
24 | github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
25 | github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
26 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
27 | github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
28 | github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
29 | github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
30 | github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
31 | github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
32 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
35 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
36 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
37 | github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
38 | github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
39 | github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
40 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
41 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
42 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
43 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
44 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
45 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
46 | github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
47 | github.com/go-ini/ini v1.57.0 h1:Qwzj3wZQW+Plax5Ntj+GYe07DfGj1OH+aL1nMTMaNow=
48 | github.com/go-ini/ini v1.57.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
49 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
50 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
51 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
52 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
53 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
54 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
55 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
56 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
57 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
58 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
59 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
60 | github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
61 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
62 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
63 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
64 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
65 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
66 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
67 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
68 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
69 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
70 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
71 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
72 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
73 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
74 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
75 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
76 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
77 | github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
78 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
79 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
80 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
81 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
82 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
83 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
84 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
85 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
86 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
87 | github.com/jinzhu/gorm v1.9.15 h1:OdR1qFvtXktlxk73XFYMiYn9ywzTwytqe4QkuMRqc38=
88 | github.com/jinzhu/gorm v1.9.15/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
89 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
90 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
91 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
92 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
93 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
94 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
95 | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
96 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
97 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
98 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
99 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
100 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
101 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
102 | github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
103 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
104 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
105 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
106 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
107 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
108 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
109 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
110 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
111 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
112 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
113 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
114 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
115 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
116 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
117 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
118 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
119 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
120 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
121 | github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
122 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
123 | github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
124 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
125 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
126 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
127 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
128 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
129 | github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
130 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
131 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
132 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
133 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
134 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
135 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
136 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
137 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
138 | github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo=
139 | github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
140 | github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
141 | github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s=
142 | github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
143 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
144 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
145 | github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
146 | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
147 | github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
148 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
149 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
150 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
151 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
152 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
153 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
154 | github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
155 | github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
156 | github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
157 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
158 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
159 | github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
160 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
161 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
162 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
163 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
164 | github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
165 | github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
166 | github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
167 | github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
168 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
169 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
170 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
171 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
172 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
173 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
174 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
175 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
176 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
177 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
178 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
179 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
180 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
181 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
182 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
183 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
184 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
185 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
186 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
187 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
188 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
189 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
190 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
191 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
192 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
193 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
194 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
195 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
196 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
197 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80=
198 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
199 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
200 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
201 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
202 | golang.org/x/tools v0.0.0-20200117065230-39095c1d176c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
203 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
204 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
205 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
206 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
207 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
208 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
209 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
210 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
211 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
212 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
213 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
214 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
215 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
216 | gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 h1:N46iQqOtHry7Hxzb9PGrP68oovQmj7EhudNoKHvbOvI=
217 | gopkg.in/dgrijalva/jwt-go.v3 v3.2.0/go.mod h1:hdNXC2Z9yC029rvsQ/on2ZNQ44Z2XToVhpXXbR+J05A=
218 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
219 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
220 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
221 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
222 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
223 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
224 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
225 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
226 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
227 |
--------------------------------------------------------------------------------