MOKASZ\lw12420 5 лет назад
Сommit
2132f56c15
100 измененных файлов с 4752 добавлено и 0 удалено
  1. BIN
      .DS_Store
  2. 7 0
      .env
  3. 7 0
      .env.prod
  4. 7 0
      .env.test
  5. 199 0
      .eslintrc.js
  6. 19 0
      .gitignore
  7. 8 0
      Dockerfile
  8. 1 0
      README.md
  9. 5 0
      babel.config.js
  10. 38 0
      docs/conf/conf.d/front.conf
  11. 36 0
      docs/conf/nginx.conf
  12. 12 0
      fw-sample-portal.iml
  13. 70 0
      package.json
  14. 89 0
      pom.xml
  15. BIN
      public/favicon.ico
  16. 20 0
      public/index.html
  17. 12 0
      src/App.vue
  18. BIN
      src/assets/image/bg-login.png
  19. BIN
      src/assets/image/icon/icon-account-edit.png
  20. BIN
      src/assets/image/icon/icon-account-user.png
  21. BIN
      src/assets/image/icon/icon-add-user-active.png
  22. BIN
      src/assets/image/icon/icon-add-user.png
  23. BIN
      src/assets/image/icon/icon-common-tip.png
  24. BIN
      src/assets/image/icon/icon-default-avatar.png
  25. BIN
      src/assets/image/icon/icon-delete-active.png
  26. BIN
      src/assets/image/icon/icon-delete.png
  27. BIN
      src/assets/image/icon/icon-login-ding.png
  28. BIN
      src/assets/image/icon/icon-login-fly.png
  29. BIN
      src/assets/image/icon/icon-login-phone-active.png
  30. BIN
      src/assets/image/icon/icon-login-phone.png
  31. BIN
      src/assets/image/icon/icon-login-type-active.png
  32. BIN
      src/assets/image/icon/icon-login-type.png
  33. BIN
      src/assets/image/icon/icon-login-veritify-active.png
  34. BIN
      src/assets/image/icon/icon-login-veritify.png
  35. BIN
      src/assets/image/icon/icon-material-edit-default.png
  36. BIN
      src/assets/image/icon/icon-menu--account.png
  37. BIN
      src/assets/image/icon/icon-nav-search-hover.png
  38. BIN
      src/assets/image/icon/icon-nav-search-normal.png
  39. BIN
      src/assets/image/icon/icon-reback.png
  40. BIN
      src/assets/image/icon/icon-refresh.png
  41. BIN
      src/assets/image/icon/icon-sort-down-active.png
  42. BIN
      src/assets/image/icon/icon-sort-down.png
  43. BIN
      src/assets/image/icon/icon-sort-up-active.png
  44. BIN
      src/assets/image/icon/icon-sort-up.png
  45. BIN
      src/assets/image/icon/icon-template-gallery-phone.png
  46. BIN
      src/assets/image/system-book.png
  47. BIN
      src/assets/image/system-logo.png
  48. 202 0
      src/assets/style/common.scss
  49. 444 0
      src/assets/style/element-variables.scss
  50. 5 0
      src/assets/style/index.scss
  51. 119 0
      src/assets/style/mixin.scss
  52. 48 0
      src/assets/style/transition.scss
  53. 6 0
      src/assets/style/variables.scss
  54. 13 0
      src/common/directive/permission/index.js
  55. 21 0
      src/common/directive/permission/permission.js
  56. 77 0
      src/common/mixin/index.js
  57. 92 0
      src/common/plugins/index.js
  58. 8 0
      src/common/utils/auth-node.js
  59. 9 0
      src/common/utils/constant.js
  60. 20 0
      src/common/utils/has-permission.js
  61. 43 0
      src/common/utils/index.js
  62. 127 0
      src/common/utils/validate.js
  63. 122 0
      src/components/ConfirmBox.vue
  64. 43 0
      src/components/DataEmpty.vue
  65. 46 0
      src/components/Err404.vue
  66. 33 0
      src/components/Icon.vue
  67. 289 0
      src/components/ImportBox.vue
  68. 68 0
      src/components/MultiSelectTip.vue
  69. 184 0
      src/components/Pagination.vue
  70. 122 0
      src/components/UploadAvatarImg.vue
  71. 317 0
      src/components/UploadImg.vue
  72. 118 0
      src/components/layout/Layout.vue
  73. 219 0
      src/components/layout/NavBar.vue
  74. 124 0
      src/components/layout/SideBar.vue
  75. 179 0
      src/components/layout/SidebarItem.vue
  76. 31 0
      src/main.js
  77. 18 0
      src/network/api/adminLog.js
  78. 39 0
      src/network/api/api-base.js
  79. 22 0
      src/network/api/dict.js
  80. 17 0
      src/network/api/system.js
  81. 81 0
      src/network/service/service.js
  82. 59 0
      src/router/index.js
  83. 22 0
      src/router/permission.js
  84. 22 0
      src/store/index.js
  85. 32 0
      src/store/modules/menu.js
  86. 72 0
      src/store/modules/store-user.js
  87. BIN
      src/views/.DS_Store
  88. 109 0
      src/views/adminLog/AdminLogMain.vue
  89. 116 0
      src/views/adminLog/OptionDialog.vue
  90. 77 0
      src/views/adminLog/Search.vue
  91. 3 0
      src/views/adminLog/index.js
  92. 27 0
      src/views/adminLog/object.js
  93. 115 0
      src/views/dict/DictMain.vue
  94. 101 0
      src/views/dict/OptionDialog.vue
  95. 53 0
      src/views/dict/Search.vue
  96. 3 0
      src/views/dict/index.js
  97. 23 0
      src/views/dict/object.js
  98. 12 0
      src/views/home/Home.vue
  99. 3 0
      src/views/redis/index.js
  100. 67 0
      src/views/redis/redisManager.vue

+ 7 - 0
.env

@@ -0,0 +1,7 @@
+# 开发环境
+
+NODE_ENV=development
+
+VUE_APP_UPLOAD_URL = /api/msg-lib-app/message/library/upload
+
+VUE_APP_API_URL = http://localhost.mokamrp.com:8080/

+ 7 - 0
.env.prod

@@ -0,0 +1,7 @@
+# 生产环境
+
+NODE_ENV=production
+
+VUE_APP_UPLOAD_URL = /api/free-novel/file/upload
+
+VUE_APP_API_URL = https://mokamrp.com

+ 7 - 0
.env.test

@@ -0,0 +1,7 @@
+# 测试环境
+
+NODE_ENV=testing
+
+VUE_APP_UPLOAD_URL = /api/free-novel/file/upload
+
+VUE_APP_API_URL = https://test.mokamrp.com/

+ 199 - 0
.eslintrc.js

@@ -0,0 +1,199 @@
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: 'babel-eslint',
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  extends: ['plugin:vue/recommended', 'eslint:recommended'],
+
+  // add your custom rules here
+  //it is base on https://github.com/vuejs/eslint-config-vue
+  rules: {
+    "vue/max-attributes-per-line": [2, {
+      "singleline": 10,
+      "multiline": {
+        "max": 1,
+        "allowFirstLine": false
+      }
+    }],
+    "vue/singleline-html-element-content-newline": "off",
+    "vue/multiline-html-element-content-newline": "off",
+    "vue/name-property-casing": ["error", "PascalCase"],
+    "vue/no-v-html": "off",
+    'accessor-pairs': 2,
+    'arrow-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'block-spacing': [2, 'always'],
+    'brace-style': [2, '1tbs', {
+      'allowSingleLine': true
+    }],
+    'camelcase': [0, {
+      'properties': 'always'
+    }],
+    'comma-dangle': [2, 'never'],
+    'comma-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'comma-style': [2, 'last'],
+    'constructor-super': 2,
+    'curly': [2, 'multi-line'],
+    'dot-location': [2, 'property'],
+    'eol-last': 2,
+    'eqeqeq': ["error", "always", { "null": "ignore" }],
+    'generator-star-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'handle-callback-err': [2, '^(err|error)$'],
+    'indent': [2, 2, {
+      'SwitchCase': 1
+    }],
+    'jsx-quotes': [2, 'prefer-single'],
+    'key-spacing': [2, {
+      'beforeColon': false,
+      'afterColon': true
+    }],
+    'keyword-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'new-cap': [2, {
+      'newIsCap': true,
+      'capIsNew': false
+    }],
+    'new-parens': 2,
+    'no-array-constructor': 2,
+    'no-caller': 2,
+    'no-console': 'off',
+    'no-class-assign': 2,
+    'no-cond-assign': 2,
+    'no-const-assign': 2,
+    'no-control-regex': 0,
+    'no-delete-var': 2,
+    'no-dupe-args': 2,
+    'no-dupe-class-members': 2,
+    'no-dupe-keys': 2,
+    'no-duplicate-case': 2,
+    'no-empty-character-class': 2,
+    'no-empty-pattern': 2,
+    'no-eval': 2,
+    'no-ex-assign': 2,
+    'no-extend-native': 2,
+    'no-extra-bind': 2,
+    'no-extra-boolean-cast': 2,
+    'no-extra-parens': [2, 'functions'],
+    'no-fallthrough': 2,
+    'no-floating-decimal': 2,
+    'no-func-assign': 2,
+    'no-implied-eval': 2,
+    'no-inner-declarations': [2, 'functions'],
+    'no-invalid-regexp': 2,
+    'no-irregular-whitespace': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-labels': [2, {
+      'allowLoop': false,
+      'allowSwitch': false
+    }],
+    'no-lone-blocks': 2,
+    'no-mixed-spaces-and-tabs': 2,
+    'no-multi-spaces': 2,
+    'no-multi-str': 2,
+    'no-multiple-empty-lines': [2, {
+      'max': 1
+    }],
+    'no-native-reassign': 2,
+    'no-negated-in-lhs': 2,
+    'no-new-object': 2,
+    'no-new-require': 2,
+    'no-new-symbol': 2,
+    'no-new-wrappers': 2,
+    'no-obj-calls': 2,
+    'no-octal': 2,
+    'no-octal-escape': 2,
+    'no-path-concat': 2,
+    'no-proto': 2,
+    'no-redeclare': 2,
+    'no-regex-spaces': 2,
+    'no-return-assign': [2, 'except-parens'],
+    'no-self-assign': 2,
+    'no-self-compare': 2,
+    'no-sequences': 2,
+    'no-shadow-restricted-names': 2,
+    'no-spaced-func': 2,
+    'no-sparse-arrays': 2,
+    'no-this-before-super': 2,
+    'no-throw-literal': 2,
+    'no-trailing-spaces': 2,
+    'no-undef': 2,
+    'no-undef-init': 2,
+    'no-unexpected-multiline': 2,
+    'no-unmodified-loop-condition': 2,
+    'no-unneeded-ternary': [2, {
+      'defaultAssignment': false
+    }],
+    'no-unreachable': 2,
+    'no-unsafe-finally': 2,
+    'no-unused-vars': [2, {
+      'vars': 'all',
+      'args': 'none'
+    }],
+    'no-useless-call': 2,
+    'no-useless-computed-key': 2,
+    'no-useless-constructor': 2,
+    'no-useless-escape': 0,
+    'no-whitespace-before-property': 2,
+    'no-with': 2,
+    'one-var': [2, {
+      'initialized': 'never'
+    }],
+    'operator-linebreak': [2, 'after', {
+      'overrides': {
+        '?': 'before',
+        ':': 'before'
+      }
+    }],
+    'padded-blocks': [2, 'never'],
+    'quotes': [2, 'single', {
+      'avoidEscape': true,
+      'allowTemplateLiterals': true
+    }],
+    'semi': [2, 'never'],
+    'semi-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'space-before-blocks': [2, 'always'],
+    'space-before-function-paren': 0,
+    'space-in-parens': [2, 'never'],
+    'space-infix-ops': 2,
+    'space-unary-ops': [2, {
+      'words': true,
+      'nonwords': false
+    }],
+    'spaced-comment': [2, 'always', {
+      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
+    }],
+    'template-curly-spacing': [2, 'never'],
+    'use-isnan': 2,
+    'valid-typeof': 2,
+    'wrap-iife': [2, 'any'],
+    'yield-star-spacing': [2, 'both'],
+    'yoda': [2, 'never'],
+    'prefer-const': 2,
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'object-curly-spacing': [2, 'always', {
+      objectsInObjects: false
+    }],
+    'array-bracket-spacing': [2, 'never'],
+    'linebreak-style': ["off", "windows"]
+  }
+}

+ 19 - 0
.gitignore

@@ -0,0 +1,19 @@
+node_modules/
+.history
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+package-lock.json
+yarn.lock

+ 8 - 0
Dockerfile

@@ -0,0 +1,8 @@
+FROM  shanbumin/nginx:1.14-alpine3.8
+
+#ADD
+ADD ./dist /usr/share/nginx/html/public/
+ADD ./docs/conf/nginx.conf /etc/nginx/nginx.conf
+ADD ./docs/conf/conf.d/front.conf /etc/nginx/conf.d/default.conf
+
+ENTRYPOINT ["nginx","-g","daemon off;"]

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# fw-sample-portal

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

+ 38 - 0
docs/conf/conf.d/front.conf

@@ -0,0 +1,38 @@
+server{
+    listen 80;
+    server_name  localhost;
+    client_max_body_size 1020M;
+    root /usr/share/nginx/html/public;
+    index  index.html index.htm;
+
+    location /  {
+              proxy_set_header    Host             $host;
+              proxy_set_header    X-Real-IP        $remote_addr;
+              proxy_set_header    X-Forwarded-For  $proxy_add_x_forwarded_for;
+              proxy_set_header    X-Client-Verify  SUCCESS;
+              proxy_set_header    X-Client-DN      $ssl_client_s_dn;
+              proxy_set_header    X-SSL-Subject    $ssl_client_s_dn;
+              proxy_set_header    X-SSL-Issuer     $ssl_client_i_dn;
+              proxy_read_timeout 1800;
+              proxy_connect_timeout 1800;
+             try_files $uri $uri/ /index.html;
+
+
+                add_header Access-Control-Allow-Origin $http_origin;
+                add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
+                add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
+
+               if ($request_method = 'OPTIONS') {
+                    return 204;
+                 }
+             }
+
+
+    location /yg/ {
+        proxy_pass http://hyperion-free-novel-yangguang-admin-svc/;
+    }
+
+    location /api/ {
+        proxy_pass http://hyperion-gateway-svc/;
+    }
+}

+ 36 - 0
docs/conf/nginx.conf

@@ -0,0 +1,36 @@
+user  nginx;
+worker_processes  auto;
+
+error_log  /var/log/nginx/error.log warn;
+pid        /var/run/nginx.pid;
+
+
+events {
+    worker_connections  20480;
+}
+
+
+http {
+    include       /etc/nginx/mime.types;
+    default_type  application/octet-stream;
+
+    fastcgi_buffer_size 128k;
+    fastcgi_buffers 256 16k;
+    client_body_buffer_size 1024k;
+
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                      '$status $body_bytes_sent "$http_referer" '
+                      '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log  /var/log/nginx/access.log  main;
+
+    sendfile        on;
+    #tcp_nopush     on;
+
+    keepalive_timeout  65;
+
+    gzip  on;
+
+    include /etc/nginx/conf.d/*.conf;
+
+}

+ 12 - 0
fw-sample-portal.iml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
+    <output url="file://$MODULE_DIR$/target/classes" />
+    <output-test url="file://$MODULE_DIR$/target/test-classes" />
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/target" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 70 - 0
package.json

@@ -0,0 +1,70 @@
+{
+  "name": "moka_free_novel_front",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "test": "vue-cli-service build --mode test",
+    "build": "vue-cli-service build --mode prod",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "core-js": "^3.6.5",
+    "element-ui": "^2.15.1",
+    "vue": "^2.6.11",
+    "vue-router": "^3.0.3",
+    "vuex": "^3.6.2",
+    "vue-video-player": "^5.0.2"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "axios": "^0.21.1",
+    "babel-eslint": "^10.1.0",
+    "compression-webpack-plugin": "^6.1.1",
+    "eslint": "^6.8.0",
+    "eslint-plugin-vue": "^6.2.2",
+    "node-sass": "^4.9.2",
+    "sass": "^1.18.0",
+    "sass-loader": "^7.1.0",
+    "vue-template-compiler": "^2.6.11"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {
+      "vue/no-unused-components": 2,
+      "indent": "off",
+      "semi": "off",
+      "no-trailing-spaces": "off",
+      "comma-dangle": "off",
+      "no-useless-return": "off",
+      "eqeqeq": "error",
+      "no-undef": "off",
+      "new-cap": [
+        "off"
+      ],
+      "generator-star-spacing": "off",
+      "no-tabs": "off",
+      "no-unused-vars": "off",
+      "no-console": "off",
+      "no-irregular-whitespace": "off",
+      "no-debugger": "off"
+    }
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

+ 89 - 0
pom.xml

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <groupId>com.mokasz</groupId>
+    <artifactId>fw-sample</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+  <packaging>pom</packaging>
+
+  <artifactId>fw-sample-portal</artifactId>
+
+  <properties>
+    <maven.compiler.source>8</maven.compiler.source>
+    <maven.compiler.target>8</maven.compiler.target>
+  </properties>
+
+
+<!--  <build>-->
+<!--    <plugins>-->
+<!--      <plugin>-->
+<!--        <groupId>org.codehaus.mojo</groupId>-->
+<!--        <artifactId>exec-maven-plugin</artifactId>-->
+<!--        <version>1.6.0</version>-->
+<!--        <executions>-->
+<!--          <execution>-->
+<!--            <id>exec-npm-run-install</id>-->
+<!--            <phase>prepare-package</phase>-->
+<!--            <goals>-->
+<!--              <goal>exec</goal>-->
+<!--            </goals>-->
+<!--            <configuration>-->
+<!--              <executable>npm</executable>-->
+<!--              <arguments>-->
+<!--                <argument>install</argument>-->
+<!--              </arguments>-->
+<!--            </configuration>-->
+<!--          </execution>-->
+<!--          <execution>-->
+<!--            <id>exec-npm-run-build</id>-->
+<!--            <phase>prepare-package</phase>-->
+<!--            <goals>-->
+<!--              <goal>exec</goal>-->
+<!--            </goals>-->
+<!--            <configuration>-->
+<!--              <executable>npm</executable>-->
+<!--              <arguments>-->
+<!--                <argument>run</argument>-->
+<!--                <argument>build:${spring.profiles.active}</argument>-->
+<!--              </arguments>-->
+<!--            </configuration>-->
+<!--          </execution>-->
+<!--        </executions>-->
+<!--      </plugin>-->
+<!--    </plugins>-->
+<!--  </build>-->
+
+  <profiles>
+    <profile>
+      <id>dev</id>
+      <properties>
+        <spring.profiles.active>dev</spring.profiles.active>
+      </properties>
+      <activation>
+        <activeByDefault>true</activeByDefault>
+      </activation>
+    </profile>
+    <profile>
+      <id>qa</id>
+      <properties>
+        <spring.profiles.active>qa</spring.profiles.active>
+      </properties>
+    </profile>
+    <profile>
+      <id>stage</id>
+      <properties>
+        <spring.profiles.active>stage</spring.profiles.active>
+      </properties>
+    </profile>
+    <profile>
+      <id>prod</id>
+      <properties>
+        <spring.profiles.active>prod</spring.profiles.active>
+      </properties>
+    </profile>
+  </profiles>
+</project>

BIN
public/favicon.ico


+ 20 - 0
public/index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="">
+
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title>投放平台</title>
+</head>
+
+<body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+</body>
+
+</html>

+ 12 - 0
src/App.vue

@@ -0,0 +1,12 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'App'
+}
+</script>

BIN
src/assets/image/bg-login.png


BIN
src/assets/image/icon/icon-account-edit.png


BIN
src/assets/image/icon/icon-account-user.png


BIN
src/assets/image/icon/icon-add-user-active.png


BIN
src/assets/image/icon/icon-add-user.png


BIN
src/assets/image/icon/icon-common-tip.png


BIN
src/assets/image/icon/icon-default-avatar.png


BIN
src/assets/image/icon/icon-delete-active.png


BIN
src/assets/image/icon/icon-delete.png


BIN
src/assets/image/icon/icon-login-ding.png


BIN
src/assets/image/icon/icon-login-fly.png


BIN
src/assets/image/icon/icon-login-phone-active.png


BIN
src/assets/image/icon/icon-login-phone.png


BIN
src/assets/image/icon/icon-login-type-active.png


BIN
src/assets/image/icon/icon-login-type.png


BIN
src/assets/image/icon/icon-login-veritify-active.png


BIN
src/assets/image/icon/icon-login-veritify.png


BIN
src/assets/image/icon/icon-material-edit-default.png


BIN
src/assets/image/icon/icon-menu--account.png


BIN
src/assets/image/icon/icon-nav-search-hover.png


BIN
src/assets/image/icon/icon-nav-search-normal.png


BIN
src/assets/image/icon/icon-reback.png


BIN
src/assets/image/icon/icon-refresh.png


BIN
src/assets/image/icon/icon-sort-down-active.png


BIN
src/assets/image/icon/icon-sort-down.png


BIN
src/assets/image/icon/icon-sort-up-active.png


BIN
src/assets/image/icon/icon-sort-up.png


BIN
src/assets/image/icon/icon-template-gallery-phone.png


BIN
src/assets/image/system-book.png


BIN
src/assets/image/system-logo.png


+ 202 - 0
src/assets/style/common.scss

@@ -0,0 +1,202 @@
+body {
+  padding: 0;
+  margin: 0;
+  background-color: #EFEFF4;
+  font-family: "PingFang SC","Helvetica Neue",Helvetica,"Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
+}
+
+#app {
+  //overflow: hidden;
+}
+
+.scroll {
+  &.el-scrollbar {
+    height: 100%;
+  }
+
+  .el-scrollbar__wrap {
+    overflow-x: hidden;
+  }
+}
+
+.flr {
+  float: right;
+}
+
+.fll {
+  float: left;
+}
+
+.clearfix {
+  &:after {
+    visibility: hidden;
+    display: block;
+    font-size: 0;
+    content: " ";
+    clear: both;
+    height: 0;
+  }
+}
+
+.list-table-delete {
+  white-space: nowrap;
+  .icon-delete-warning {
+    color: #FF8023;
+  }
+}
+
+
+// 表格状态样式
+.status-text {
+  color: #d9d9d9;
+  font-size: 14px;
+  cursor: pointer;
+  &:before {
+    content: '';
+    width: 4px;
+    height: 4px;
+    margin-right: 8px;
+    display: inline-block;
+    border-radius: 50%;
+    background-color: #d9d9d9;
+    position: relative;
+    top: -2px;
+  }
+  &.status-progress {
+    color: #4C88F6;
+    &:before {
+      background-color: #4C88F6;
+    }
+  }
+  &.status-success {
+    color: #67c23a;
+    &:before {
+      background-color: #67c23a;
+    }
+  }
+  &.status-danger {
+    color: #FB4444;
+    &:before {
+      background-color: #FB4444;
+    }
+  }
+}
+
+.single-page-container {
+  .page-course-section {
+    //background: #FFFFFF;
+    //box-shadow: 0px 2px 2px 0px rgba(174, 175, 175, 0.2);
+    //border-radius: 2px;
+    .el-input-number__increase, .el-input-number__decrease {
+      background: transparent;
+    }
+    margin-bottom: 20px;
+    position: relative;
+
+    .title {
+      height: 56px;
+      line-height: 56px;
+      border-bottom: solid 1px #EBEBEB;
+      box-sizing: border-box;
+      color: #333A3F;
+      font-size: 18px;
+      font-weight: 600;
+      &:before {
+        content: '';
+        display: inline-block;
+        width: 3px;
+        height: 16px;
+        background-color: $themeColor;
+        margin-right: 5px;
+        position: relative;
+        top: 2px;
+      }
+    }
+
+    .page-course-content {
+      padding: 18px 40px 18px 37px;
+      & > .el-form-item {
+        .el-input:not(.el-transfer-panel__filter), .el-textarea, .el-select{
+          width: 400px;
+        }
+        .el-input-number .el-input {
+          width: auto;
+        }
+      }
+
+      .section-transfer-container {
+        .el-transfer-panel .el-transfer-panel__header {
+          background: #fff;
+        }
+        .el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label {
+          font-size: 12px;
+          color: #1A1414;
+        }
+      }
+    }
+
+    .item-tips {
+      width: 400px;
+      color: #666;
+      font-size: 12px;
+
+      i {
+        color: #FFB567;
+      }
+
+      span {
+        color: $themeColor;
+      }
+    }
+  }
+
+  .page-course-operate {
+    text-align: center;
+    margin-bottom: 50px;
+  }
+
+  .reback-btn {
+    position: absolute;
+    right: 0;
+    top: 5px;
+
+    a {
+      display: inline-block;
+      width: 70px;
+      height: 36px;
+      line-height: 36px;
+      border: solid 1px #E5E9EC;
+      box-sizing: border-box;
+      color: #333;
+      font-size: 14px;
+      background-color: #fff;
+      text-align: center;
+      text-decoration: none;
+      transition: all 0.1s;
+      border-radius: 2px;
+
+      i {
+        display: inline-block;
+        width: 16px;
+        height: 16px;
+        position: relative;
+        top: 5px;
+        background-image: url("~@/assets/image/icon/icon-reback.png");
+        background-repeat: no-repeat;
+        background-position: 0 0;
+        background-size: 42px 80%;
+        margin-right: 5px;
+      }
+
+      &:hover {
+        color: $themeColor;
+        border-color: $themeColor;
+        background-color: #ECF1FE;
+
+        i {
+          background-position: -28px 0;
+        }
+      }
+    }
+  }
+}

+ 444 - 0
src/assets/style/element-variables.scss

@@ -0,0 +1,444 @@
+/*
+Write your variables here. All available variables can be
+found in element-ui/packages/theme-chalk/src/common/var.scss.
+For example, to overwrite the theme color:
+*/
+$--color-primary: $themeColor;
+
+/* icon font path, required */
+$--font-path: '~element-ui/lib/theme-chalk/fonts';
+
+@import "../../../node_modules/element-ui/packages/theme-chalk/src/index";
+
+// el-dialog
+
+.el-dialog__wrapper {
+  .el-dialog {
+    border-radius: 8px;
+
+    .el-dialog__header {
+      border-bottom: 1px solid rgba(235, 235, 235, 1);
+      padding-top: 15px;
+      padding-bottom: 15px;
+      padding-left: 24px;
+    }
+
+    .el-dialog__body {
+      .el-form {
+        width: 90%;
+        margin: auto;
+      }
+    }
+
+    // title 样式设置
+    .dialog-title {
+      font-size: 14px;
+      font-family: 'Microsoft YaHei';
+      font-weight: 400;
+      color: #7D7D7D;
+      line-height: 24px;
+    }
+
+    // footer 中按钮的布局样式
+    .dialog-footer {
+      @include flex-center;
+      padding-bottom: 10px;
+
+      .btn {
+        margin-left: 20px;
+
+        &:first-child {
+          margin-left: 0;
+        }
+
+        &.el-button.is-round {
+          border-radius: 18px;
+          padding: 10px 18px;
+        }
+
+        &.el-button.el-button--default {
+          color: #1E1E1E;
+          background: #fff;
+          border: 1px solid #F2F2F5;
+        }
+      }
+    }
+  }
+}
+
+
+// 弹窗组件样式修改
+.dialog-form {
+  .el-select {
+    width: 100%;
+  }
+
+  .el-input__inner {
+    border: 1px solid #EDECF2;
+    border-radius: 3px;
+    height: 32px;
+    line-height: 32px;
+    font-size: 14px;
+
+    &:focus {
+      border-color: $themeColor;
+    }
+
+    &::-webkit-input-placeholder {
+      color: rgba(212, 212, 212, 1);
+    }
+
+    // 日期范围选择(el-date-picker)图标 line-height
+    &.el-date-editor.el-date-editor--daterange, &.el-date-editor.el-date-editor--datetimerange, &.el-date-editor.el-date-editor--timerange {
+      .el-input__icon.el-range__icon {
+        line-height: 22px;
+      }
+
+      .el-input__icon.el-range__close-icon {
+        line-height: 22px;
+      }
+
+      .el-range-separator {
+        line-height: 22px;
+      }
+    }
+  }
+
+  .el-input__icon {
+    line-height: 32px;
+  }
+
+  .el-range-input { // 时间范围选取时输入框的字体设置
+    font-size: 14px;
+
+    &::-webkit-input-placeholder {
+      color: rgba(212, 212, 212, 1);
+    }
+  }
+
+  // 选择框 focus 和 hover 时边框的颜色
+  .el-select .el-input__inner:focus {
+    border-color: $themeColor;
+  }
+
+  .el-select:hover .el-input__inner {
+    border-color: $themeColor;
+  }
+
+  .el-textarea {
+    .el-textarea__inner {
+      border: 1px solid #EDECF2;
+      border-radius: 2px;
+
+      &::-webkit-input-placeholder {
+        color: rgba(212, 212, 212, 1);
+      }
+    }
+  }
+
+  .el-radio {
+    color: #1E1E1E;
+    font-weight: 400;
+  }
+
+  .el-input-number__increase, .el-input-number__decrease {
+    background: transparent;
+  }
+
+  // el-form-item 中 label 的字体、颜色设置
+  .el-form-item.title-label {
+    //margin-bottom: 12px;
+
+    .el-form-item__label {
+      height: 32px;
+      line-height: 32px;
+      font-size: 14px;
+      font-weight: 400;
+      color: #474747;
+      position: relative;
+    }
+
+    .el-form-item__content {
+      line-height: 32px;
+    }
+  }
+
+  // el-cascader 输入框样式
+  .el-cascader.el-cascader--small {
+    line-height: 30px;
+    margin-top: -1px;
+
+    & .el-input__inner:focus {
+      border-color: $themeColor;
+    }
+
+    &:hover .el-input__inner {
+      border-color: #31C37B;
+    }
+
+    .el-input__inner {
+      border: 1px solid rgba(220, 223, 230, 1);
+      border-radius: 3px;
+      height: 30px;
+      line-height: 30px;
+      font-size: 14px;
+      box-sizing: border-box;
+
+      &::-webkit-input-placeholder {
+        color: rgba(212, 212, 212, 1);
+      }
+    }
+  }
+}
+
+.search-form {
+  .el-input__inner {
+    border: 1px solid #EDECF2;
+    border-radius: 18px;
+
+    &:focus {
+      border-color: $themeColor;
+    }
+
+    &::-webkit-input-placeholder {
+      color: #A8ACB4;
+    }
+  }
+
+  .el-button {
+    border-radius: 18px;
+  }
+
+  .el-form-item {
+    position: relative;
+    .el-form-item__label {
+      position: absolute;
+      top: -18px;
+      left: 22px;
+      color: #A8ACB4;
+      font-size: 10px;
+      z-index: 1;
+      background-color: #fff;
+      height: 25px;
+      padding-left: 5px;
+      text-align: center;
+    }
+  }
+}
+
+.round-select {
+  .el-input__inner {
+    border: 1px solid #EDECF2;
+    border-radius: 18px;
+
+    &:focus {
+      border-color: $themeColor;
+    }
+
+    &::-webkit-input-placeholder {
+      color: #A8ACB4;
+    }
+  }
+}
+
+.operate-box {
+  margin: 19px 0 13px 0;
+
+  .el-button {
+    padding: 10px 8px;
+    font-weight: 400;
+    .el-icon-plus {
+      font-weight: bold;
+    }
+  }
+}
+
+.list-table {
+
+  &::before, .el-table__fixed::before, .el-table__fixed-right::before { // 去掉表格下方的线
+    height: 0;
+  }
+
+  &.el-table th.is-leaf, &.el-table td {
+    border-bottom: none;
+  }
+
+  &.el-table thead {
+    color: #1E1E1E;
+    font-weight: 400;
+  }
+
+  &.el-table th {
+    background-color: #FBFAFD;
+  }
+
+  &.el-table th > .cell {
+    font-weight: 400;
+  }
+
+  &.el-table--striped .el-table__body tr.el-table__row--striped td {
+    background: #FBFAFD;
+  }
+
+  // 鼠标移上时,当前行背景色(固定了列的 el-table,鼠标移上时,tr 标签上会多一个 .hover-row )
+  .el-table__body tr.el-table__row.hover-row {
+    > td {
+      background-color: #F2F7FF;
+
+      //&:first-child { // 鼠标移上时,在表格最左侧显示一个标记
+      //  &::before {
+      //    content: '';
+      //    position: absolute;
+      //    width: 4px;
+      //    left: 0;
+      //    top: 0;
+      //    bottom: 0;
+      //    background-color: $themeColor;
+      //    z-index: 10;
+      //  }
+      //}
+    }
+  }
+
+  // 鼠标移上时,当前行背景色(正常 el-table)
+  &.el-table--enable-row-hover .el-table__body tr.el-table__row {
+    &:hover {
+      > td {
+        background-color: #F2F7FF;
+
+        //&:first-child { // 鼠标移上时,在表格最左侧显示一个标记
+        //  &::before {
+        //    content: '';
+        //    position: absolute;
+        //    width: 4px;
+        //    left: 0;
+        //    top: 0;
+        //    bottom: 0;
+        //    background-color: $themeColor;
+        //    z-index: 10;
+        //  }
+        //}
+      }
+    }
+  }
+
+  .el-table__body .cell {
+    color: rgba(71, 71, 71, 0.8);
+    line-height: 25px;
+  }
+
+  .el-table__header .cell {
+    line-height: 25px;
+  }
+
+  .el-button {
+    .underline {
+      display: inline-block;
+      border-bottom: solid 1px rgba(40, 66, 200, 0.7);
+      color: rgba(40, 66, 200, 0.7);
+    }
+
+    &:hover {
+      .underline {
+        color: $themeColor;
+        border-bottom: solid 1px $themeColor;
+      }
+    }
+  }
+
+  &.el-table--scrollable-x .el-table__body-wrapper {
+    @include scrollBar;
+  }
+
+  &.el-table--scrollable-y .el-table__body-wrapper {
+    @include scrollBar;
+  }
+
+  .el-image__error {
+    font-size: 12px;
+  }
+
+}
+
+// tab panel
+.el-tabs {
+  &.el-tabs--card > .el-tabs__header .el-tabs__nav {
+    border-left-color: transparent;
+    border-right-color: transparent;
+    border-top-color: transparent;
+    border-radius: 0;
+  }
+
+  &.el-tabs--card > .el-tabs__header .el-tabs__item {
+    border-left-color: transparent;
+  }
+
+  &.el-tabs--card > .el-tabs__header {
+    border-bottom: solid 1px #F7F7F7;
+  }
+
+  .el-tabs__item {
+    font-size: 16px;
+    font-weight: 300;
+    color: #B0B0B0;
+    transition: all 0.3s;
+    user-select: none;
+    border-right: solid 1px transparent;
+
+    &.is-active {
+      border-top: solid 4px $themeColor;
+      box-sizing: content-box;
+      color: $themeColor;
+      font-weight: 400;
+
+      &:first-child {
+        border-right-color: #F7F7F7;
+      }
+
+      &:not(:first-child) {
+        border-left-color: #F7F7F7;
+        border-right-color: #F7F7F7;
+      }
+    }
+  }
+
+  .el-tabs__content {
+    padding: 0 18px;
+  }
+}
+
+.noborder-input {
+  .el-input__inner {
+    border: none;
+  }
+}
+
+.tab-menu-container {
+  padding-left: 20px;
+
+  .el-menu-item {
+    font-size: 16px;
+    font-weight: bold;
+    cursor: pointer;
+  }
+
+  &.el-menu--horizontal > .el-menu-item.is-active {
+    border-width: 4px;
+    color: $themeColor;
+    position: relative;
+
+    &:before {
+      content: '';
+      display: inline-block;
+      width: 6px;
+      height: 6px;
+      background-color: $themeColor;
+      border-radius: 50%;
+      position: absolute;
+      left: 6px;
+      top: 50%;
+      margin-right: 5px;
+    }
+  }
+}
+

+ 5 - 0
src/assets/style/index.scss

@@ -0,0 +1,5 @@
+@import "variables";
+@import "mixin";
+@import "common";
+@import "element-variables";
+@import "transition";

+ 119 - 0
src/assets/style/mixin.scss

@@ -0,0 +1,119 @@
+// flex 方式居中
+@mixin flex-center {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+}
+
+// flex 方式水平布局居中
+@mixin flex-horizontal-center {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+// flex 方式垂直布局居中
+@mixin flex-vertical-center {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+// 鼠标移上时显示白色蒙层
+@mixin mouse-hover-mask {
+  &:hover {
+    &::after {
+      content: '';
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      top: 0;
+      background-color: rgba(255, 255, 255, 0.3);
+    }
+  }
+}
+
+// 滚动条设置
+@mixin scrollBar {
+  /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
+  &::-webkit-scrollbar {
+    width: 10px;
+    height: 10px;
+    background-color: #F6F6F6;
+  }
+  /*定义滚动条轨道*/
+  &::-webkit-scrollbar-track {
+    -webkit-box-shadow: inset 0 0 6px transparent;
+    border-radius: 0;
+    background-color: #F5F5F5;
+  }
+  /*定义滑块*/
+  &::-webkit-scrollbar-thumb {
+    background-color: #D4D4D4;
+  }
+  /*定义滚动条两端按钮*/
+  &::-webkit-scrollbar-button {
+    display: block;
+    width: 10px;
+  }
+  &::-webkit-scrollbar-track:horizontal {
+    padding: 0;
+    margin: 0;
+  }
+  //&::-webkit-scrollbar-button:horizontal:single-button:start {
+  //  background: url(~@/assets/img/icon/icon-scroll-left.png) 0 0 no-repeat;
+  //  cursor: pointer;
+  //}
+  //&::-webkit-scrollbar-button:horizontal:single-button:end {
+  //  text-align: right;
+  //  background: url(~@/assets/img/icon/icon-scroll-right.png) 0 0 no-repeat;
+  //  cursor: pointer;
+  //}
+}
+
+// 页面内容 div 使用,设置了圆角和阴影
+@mixin page-content-wrapper {
+  background-color: #fff;
+  box-shadow: 1px 2px 8px 0 rgba(223, 227, 235, 0.66);
+  border-radius: 4px;
+  box-sizing: border-box;
+  padding: 18px;
+}
+
+// 弹框内容布局(样式为左边标题,右边输入框/文字)
+@mixin dialog-content-layout {
+  box-sizing: border-box;
+
+  .row {
+    @include flex-horizontal-center;
+    margin-bottom: 24px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .label {
+      width: 80px;
+      flex-basis: 80px;
+      flex-shrink: 0;
+      text-align: right;
+      font-size: 12px;
+      font-family: "Microsoft YaHei";
+      color: rgba(125, 125, 125, 1);
+      line-height: 24px;
+    }
+
+    span {
+      font-size: 12px;
+    }
+  }
+}
+
+// label 字体设置(左名称,右内容布局时,左边的都称为 label)
+@mixin label-font {
+  font-size: 12px;
+  font-weight: 300;
+  color: #7D7D7D;
+}

+ 48 - 0
src/assets/style/transition.scss

@@ -0,0 +1,48 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+  opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+  transition: all .5s;
+}
+
+.fade-transform-enter {
+  opacity: 0;
+  transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+  transition: all .5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+.breadcrumb-move {
+  transition: all .5s;
+}
+
+.breadcrumb-leave-active {
+  position: absolute;
+}

+ 6 - 0
src/assets/style/variables.scss

@@ -0,0 +1,6 @@
+// 此文件中定义的都是全局常量
+
+// theme color
+//$themeColor: #2842C8;
+$themeColor: #409EFF;
+$sub-theme-color: #3D68DD;

+ 13 - 0
src/common/directive/permission/index.js

@@ -0,0 +1,13 @@
+import permission from './permission'
+
+const install = function (Vue) {
+  Vue.directive('has', permission)
+}
+
+if (window.Vue) {
+  window.permission = permission
+  Vue.use(install) // eslint-disable-line
+}
+
+permission.install = install
+export default permission

+ 21 - 0
src/common/directive/permission/permission.js

@@ -0,0 +1,21 @@
+export default {
+  inserted(el, binding, vnode) {
+    const { value } = binding
+    const permission = vnode.context.$store.state.user.permission
+    if (value) {
+      if (value instanceof Array) {
+        const check = value.every(v => !permission.includes(v))
+        if (check) {
+          el.parentNode && el.parentNode.removeChild(el)
+        }
+      } else {
+        if (!permission.includes(value)) {
+          // console.log('not included-------', value)
+          el.parentNode && el.parentNode.removeChild(el)
+        }
+      }
+    } else {
+      throw new Error(`need permission! Like v-has="'admin'"`)
+    }
+  }
+}

+ 77 - 0
src/common/mixin/index.js

@@ -0,0 +1,77 @@
+/**
+ * 页面公用的组件,定义在这个 mixin 中,组件中导入,减少导入代码的编写
+ */
+
+import Pagination from '@/components/Pagination'
+
+export const PageCommonComponent = {
+  components: {
+    'c-pagination': Pagination
+  },
+  data() {
+    return {
+      isSearch: false,
+      isLoading: false, // 是否正在加载数据
+      totalCount: 0, // 数据总条数
+      pageNo: 1, // 当前显示的页码
+      pageSize: 10 // 每页显示条数
+    }
+  },
+  methods: {
+    // 时间格式化
+    parseTime(time, cFormat) {
+      if (arguments.length === 0) {
+        return null
+      }
+      const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+      let date
+      if (typeof time === 'object') {
+        date = time
+      } else {
+        if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+          time = parseInt(time)
+        }
+        if ((typeof time === 'number') && (time.toString().length === 10)) {
+          time = time * 1000
+        }
+        date = new Date(time)
+      }
+      const formatObj = {
+        y: date.getFullYear(),
+        m: date.getMonth() + 1,
+        d: date.getDate(),
+        h: date.getHours(),
+        i: date.getMinutes(),
+        s: date.getSeconds(),
+        a: date.getDay()
+      }
+      const timeStr = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+        let value = formatObj[key]
+        if (key === 'a') {
+          return ['一', '二', '三', '四', '五', '六', '日'][value - 1]
+        }
+        if (result.length > 0 && value < 10) {
+          value = '0' + value
+        }
+        return value || 0
+      })
+      return timeStr
+    },
+
+    // 搜索
+    onClickSearch() {
+      if (this.pageNo === 1) {
+        this._loadData()
+      } else {
+        this.pageNo = 1
+      }
+    },
+
+    // utc时间格式修改
+    _getFormatTime(time) {
+      var d = new Date(time)
+      var times = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds()
+      return times
+    }
+  }
+}

+ 92 - 0
src/common/plugins/index.js

@@ -0,0 +1,92 @@
+/**
+ * 全局插件,包括一些方法、组件等
+ */
+
+import DataEmpty from '@/components/DataEmpty'
+import Icon from '@/components/Icon'
+
+const GlobalPlugin = {
+  install(Vue, options) {
+    /**
+     * message 成功提示
+     * @param message 信息内容
+     * @param duration 显示时间,默认 2000 毫秒,0 为一直显示
+     * @param showClose 是否显示关闭按钮,默认不显示
+     * @param offset 距离窗口顶部的距离
+     */
+    Vue.prototype.$SuccessMessage = function (message = '', duration = 2000, showClose = false, offset = 200, dangerouslyUseHTMLString = true) {
+      const finalOpt = {
+        dangerouslyUseHTMLString: dangerouslyUseHTMLString,
+        message: message,
+        duration: duration,
+        type: 'success',
+        showClose: showClose,
+        offset: offset
+      }
+      Vue.prototype.$message(finalOpt)
+    }
+
+    /**
+     * message 错误提示
+     * @param message 信息内容
+     * @param duration 显示时间,默认 3000 毫秒,0 为一直显示
+     * @param showClose 是否显示关闭按钮,默认不显示
+     * @param offset 距离窗口顶部的距离
+     */
+    Vue.prototype.$ErrorMessage = function (message = '', duration = 3000, showClose = false, offset = 200, dangerouslyUseHTMLString = true) {
+      const finalOpt = {
+        dangerouslyUseHTMLString: dangerouslyUseHTMLString,
+        message: message,
+        duration: duration,
+        type: 'error',
+        showClose: showClose,
+        offset: offset
+      }
+      Vue.prototype.$message(finalOpt)
+    }
+
+    /**
+     * message 警告提示
+     * @param message 信息内容
+     * @param duration 显示时间,默认 3000 毫秒,0 为一直显示
+     * @param showClose 是否显示关闭按钮,默认不显示
+     * @param offset 距离窗口顶部的距离
+     */
+    Vue.prototype.$WarningMessage = function (message = '', duration = 3000, showClose = false, offset = 200, dangerouslyUseHTMLString = true) {
+      const finalOpt = {
+        dangerouslyUseHTMLString: dangerouslyUseHTMLString,
+        message: message,
+        duration: duration,
+        type: 'warning',
+        showClose: showClose,
+        offset: offset
+      }
+      Vue.prototype.$message(finalOpt)
+    }
+
+    /**
+     * message 信息提示
+     * @param message 信息内容
+     * @param duration 显示时间,默认 2000 毫秒,0 为一直显示
+     * @param showClose 是否显示关闭按钮,默认不显示
+     * @param offset 距离窗口顶部的距离
+     */
+    Vue.prototype.$InfoMessage = function (message = '', duration = 2000, showClose = false, offset = 200, dangerouslyUseHTMLString = true) {
+      const finalOpt = {
+        dangerouslyUseHTMLString: dangerouslyUseHTMLString,
+        message: message,
+        duration: duration,
+        type: 'info',
+        showClose: showClose,
+        offset: offset
+      }
+      Vue.prototype.$message(finalOpt)
+    }
+
+    /* --------------------------- 全局组件 --------------------------- */
+    Vue.component('c-data-empty', DataEmpty)
+    Vue.component('c-icon', Icon)
+  }
+}
+
+export default GlobalPlugin

+ 8 - 0
src/common/utils/auth-node.js

@@ -0,0 +1,8 @@
+/**
+ * 各模块权限节点配置文件
+ */
+module.exports = {
+  novelManage: {
+    
+  }
+}

+ 9 - 0
src/common/utils/constant.js

@@ -0,0 +1,9 @@
+export const ProductTypeList = [{label: 'VIP', value: 1},{label: '阅读劵', value: 2},
+    {label: 'VIP券', value: 3},{label: '书币', value: 4}]
+
+export  const ProductTypeMapping = {
+    "1": "VIP",
+    "2": "阅读劵",
+    "3": "VIP券",
+    "4": "书币",
+}

+ 20 - 0
src/common/utils/has-permission.js

@@ -0,0 +1,20 @@
+import store from '@/store'
+
+/**
+ * @description 全局函数,table列权限无法使用v-has指令显示隐藏,只能通过v-if条件判断显示隐藏
+ * @param value
+ * @returns {boolean}
+ */
+export function hasPerm(value) {
+  const permission = store.state.user.permission
+  if (value) {
+    if (value instanceof Array) {
+      const check = value.every(v => !permission.includes(v))
+      return !check
+    } else {
+      return permission.includes(value)
+    }
+  } else {
+    return true
+  }
+}

+ 43 - 0
src/common/utils/index.js

@@ -0,0 +1,43 @@
+/**
+ * @param time datetime
+ * @param cFormat
+ * @returns {*} yyyy-mm-dd hh:min:ss
+ */
+export function parseTime (time, cFormat) {
+  if (arguments.length === 0) {
+    return null
+  }
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+      time = parseInt(time)
+    }
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const timeStr = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+    let value = formatObj[key]
+    if (key === 'a') {
+      return ['一', '二', '三', '四', '五', '六', '日'][value - 1]
+    }
+    if (result.length > 0 && value < 10) {
+      value = '0' + value
+    }
+    return value || 0
+  })
+  return timeStr
+}

+ 127 - 0
src/common/utils/validate.js

@@ -0,0 +1,127 @@
+/**
+ * IP地址验证
+ * @param val
+ * @returns {boolean} IP validate
+ */
+export function IPValidator (val = '') {
+  const reg = /^((25[0-5]|2[0-4]\d|[1]\d\d|[1-9]\d|\d)($|(?!\.$)\.)){4}$/
+  return reg.test(val)
+}
+
+/**
+ * 纯数字验证
+ * @param val value string
+ * @params min min value
+ * @param max max value
+ */
+export function numberValidate (val = '', min = 0, max = 1000) {
+  const reg = new RegExp(`^[0-9]{${min},${max}}$`)
+  return reg.test(val)
+}
+
+/**
+ * 邮箱验证
+ * @param val
+ * @returns {*|boolean}
+ */
+export function emailValidator (val = '') {
+  const reg = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}/
+  return reg.test(val)
+}
+
+/**
+ * 手机号验证
+ * @param val
+ * @returns {*|boolean}
+ */
+export function phoneValidator (val = '') {
+  const reg = /^((13[0-9])|(14[0-9])|(15[0-9])|(16[0-9])|(17[0-9])|(18[0-9])|(19[0-9]))\d{8}$/
+  return reg.test(val)
+}
+
+/**
+ * 中英文(大小写)验证
+ * @param val
+ * @param min
+ * @param max
+ * @returns {boolean}
+ */
+export function zhCNenUSValidator (val = '', min = 2, max = 50) {
+  const reg = new RegExp(`^[\u4e00-\u9fa5a-zA-Z]{${min},${max}}$`)
+  return reg.test(val)
+}
+
+/**
+ * 英文(大小写)数字验证
+ * @param val
+ * @param min
+ * @param max
+ * @returns {boolean}
+ */
+export function enUSNumValidator (val = '', min = 2, max = 50) {
+  const reg = new RegExp(`^[a-zA-Z0-9]{${min},${max}}$`)
+  return reg.test(val)
+}
+
+/**
+ * 纯中文验证
+ * @param val
+ * @param min
+ * @param max
+ * @returns {boolean}
+ */
+export function zhCNValidator (val = '', min = 2, max = 50) {
+  const reg = new RegExp(`^[\u4e00-\u9fa5]{${min},${max}}$`)
+  return reg.test(val)
+}
+
+/**
+ * @description 数字和大写英文字母验证
+ * @param val
+ * @returns {boolean}
+ */
+export function taxNumValidator (val = '', min = 18, max = 18) {
+  const reg = new RegExp(`^[A-Z0-9]{${min},${max}}$`)
+  return reg.test(val)
+}
+
+/**
+ * @description 英文 数字 下划线
+ * @param val
+ * @returns {boolean}
+ */
+export function keyValidator (val = '') {
+  const reg = new RegExp(`^[0-9A-Za-z_]*$`)
+  return reg.test(val)
+}
+
+/**
+ * @description appid验证
+ * @param val
+ * @returns {boolean}
+ * */
+export function AppIdValidator (val = '') {
+  const reg = /^wx[a-z0-9]{16}$/
+  return reg.test(val)
+}
+
+/**
+ * @description 链接验证
+ * @param val
+ * @returns {boolean}
+ */
+export function pathValidator (val) {
+  const reg = new RegExp(`^((ht|f)tps?):\\/\\/([\\w\\-]+(\\.[\\w\\-]+)*\\/)*[\\w\\-]+(\\.[\\w\\-]+)*\\/?(\\?([\\w\\-\\.,@?^=%&:\\/~\\+#]*)+)?`)
+  return reg.test(val)
+}
+
+
+/**
+ * @description ghid
+ * @param val
+ * @returns {boolean}
+ */
+export function GhIdValidator(val = '') {
+  const reg = /^gh_[a-z0-9]{12}$/
+  return reg.test(val)
+}

+ 122 - 0
src/components/ConfirmBox.vue

@@ -0,0 +1,122 @@
+<script type="text/jsx">
+export default {
+  name: 'ConfirmBox',
+  props: {
+    text: {
+      type: String,
+      default: ''
+    },
+    content: {
+      type: String,
+      default: ''
+    },
+    type: {
+      type: String,
+      default: 'confirm'
+    },
+    beforeConfirm: {
+      type: Function,
+      default () {
+        return Promise.resolve(true)
+      }
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data () {
+    return {
+      show: false,
+      icons: {
+        'confirm': {
+          icon: 'el-icon-warning',
+          color: '#2842C8'
+        },
+        'danger': {
+          icon: 'el-icon-warning',
+          color: '#FD5655'
+        },
+        'warning': {
+          icon: 'el-icon-warning',
+          color: '#FF9000'
+        }
+      }
+    }
+  },
+  methods: {
+    async showConfirmBox () {
+      if (this.disabled) {
+        return
+      }
+      const checkOperate = await this.beforeConfirm()
+      if (checkOperate) {
+        const icon = this.icons[this.type].icon
+        const color = this.icons[this.type].color
+        const kEle = `<div class="confirm-box">
+        <div>
+          <i class="${icon}" style="color: ${color}"></i>
+          <span>${this.text}</span>
+        </div>
+      <p>${this.content}</p>
+      </div>`
+        this.$confirm(`${kEle}`, {
+          dangerouslyUseHTMLString: true,
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          showClose: false,
+          roundButton: true,
+          closeOnClickModal: false,
+          confirmButtonClass: 'button-confirm',
+          cancelButtonClass: 'button-confirm',
+          center: true
+        }).then(() => {
+          this.$emit('confirm')
+        }).catch(() => {
+        })
+      }
+    }
+  },
+  render () {
+    const { showConfirmBox } = this
+    return (
+      <div onClick={showConfirmBox}>{this.$slots.default}</div>)
+  }
+}
+</script>
+
+<style lang="scss">
+.confirm-box {
+  text-align: center;
+  margin-bottom: 20px;
+
+  p {
+    font-size: 14px;
+    color: #a8acb4;
+    margin: 15px 0 15px !important;
+
+    span {
+      font-size: 16px;
+      font-weight: bold;
+      margin-right: 10px;
+      color: #31c37b;
+    }
+  }
+
+  div {
+    span {
+      font-size: 18px;
+      color: #1e1e1e;
+    }
+
+    i {
+      font-size: 18px;
+      font-weight: bold;
+    }
+  }
+}
+
+.el-message-box__btns .button-confirm {
+  width: 60px;
+}
+</style>

+ 43 - 0
src/components/DataEmpty.vue

@@ -0,0 +1,43 @@
+<!--表格空数据时显示-->
+<template>
+  <!--空数据提示-->
+  <div class="empty-data">
+    <div class="tip">{{ tip }}</div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DataEmpty',
+  props: {
+    tip: {
+      type: String,
+      default: '暂无数据'
+    }
+  },
+  data () {
+    return {}
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import "~@/assets/style/mixin";
+.empty-data {
+  @include flex-vertical-center;
+
+  .icon {
+    width: 180px;
+    height: 204px;
+  }
+
+  .tip {
+    text-align: center;
+    font-size: 16px;
+    font-family: "Microsoft YaHei";
+    font-weight: 400;
+    color: rgba(102, 102, 102, 1);
+    line-height: 24px;
+  }
+}
+</style>

+ 46 - 0
src/components/Err404.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="err-404">
+    <div class="content">
+      <h1 class="title">404</h1>
+      <p class="text">抱歉,你访问的页面不存在</p>
+      <el-button type="primary" style="width: 100px" @click="$router.push('/')">
+        返回首页</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Err404',
+  data () {
+    return {}
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.err-404 {
+  margin-top: 200px;
+
+  .content {
+    padding-left: 40px;
+    text-align: center;
+  }
+
+  .title {
+    margin-top: 80px;
+    font-size: 72px;
+    color: #434e59;
+    font-weight: 600;
+    line-height: 72px;
+    margin-bottom: 24px;
+  }
+
+  .text {
+    color: rgba(0, 0, 0, 0.45);
+    font-size: 20px;
+    line-height: 28px;
+    margin-bottom: 16px;
+  }
+}
+</style>

+ 33 - 0
src/components/Icon.vue

@@ -0,0 +1,33 @@
+<template>
+  <img
+    class="icon-img"
+    :src="require(`@/assets/image/icon/${icon}.png`)"
+    :style="{'width': width}"
+  >
+</template>
+
+<script>
+export default {
+  name: 'Icon',
+  props: {
+    icon: {
+      type: String,
+      required: true
+    },
+    width: {
+      type: String,
+      default: ''
+    }
+  },
+  data () {
+    return {}
+  }
+}
+</script>
+
+<style scoped lang="scss">
+img.icon-img {
+  position: relative;
+  top: -1px;
+}
+</style>

+ 289 - 0
src/components/ImportBox.vue

@@ -0,0 +1,289 @@
+<!--文件上传弹框-->
+<template>
+  <el-dialog class="upload-dialog" width="580px" :visible.sync="show" :modal-append-to-body="false">
+    <!--自定义 title-->
+    <span slot="title" class="dialog-title">{{ title }}</span>
+
+    <!--上传文件框-->
+    <el-upload
+      ref="import"
+      name="file"
+      class="import"
+      drag
+      :accept="accept"
+      :auto-upload="false"
+      :file-list="files"
+      :action="UPLOAD_URL"
+      :data="param"
+      :headers="uploadHeader"
+      :on-change="checkFile"
+      :on-success="uploadResult"
+      :on-error="uploadResult"
+    >
+      <div class="import-icon">
+        <c-icon style="width: 32px; height: 24px;" icon="icon-common-dialog-upload" />
+      </div>
+      <div class="el-upload__text">
+        <span>点击或将文件拖拽到这里上传</span>
+        <div class="el-upload__tip" style="color: #BEBEBE">支持扩展名:{{ accept }}</div>
+      </div>
+    </el-upload>
+
+    <!--底部操作按钮-->
+    <div slot="footer" style="text-align: right">
+      <!--提示信息-->
+      <div v-if="tip" class="tip">Tips:{{ tip }}</div>
+      <!--操作按钮-->
+      <div class="btn-wrapper">
+        <!--        <export-to-excel v-if="!hideDownload" :path="downloadFile" :is-template="true">-->
+        <!--          <el-button v-waves >模板下载</el-button>-->
+        <!--        </export-to-excel>-->
+        <el-button v-waves style="width: 60px; margin-left: 10px;" @click="cancelUpload">取消</el-button>
+        <el-button
+          v-waves
+          type="primary"
+          style="width: 60px; margin-left: 20px;"
+          :loading="loading"
+          @click="submitUpload(0)"
+        >确定
+        </el-button>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { uploadTemp } from '@/network/api/api-base'
+
+const K_MAX_SIZE = 10 * 1024 * 1024 // 单位:Byte,文件最大 10M
+
+export default {
+  name: 'ImportBox',
+  components: {
+    // ExportToExcel
+  },
+  filters: {},
+  props: {
+    // 弹框标题
+    title: {
+      type: String,
+      default: '导入'
+    },
+
+    // 引入该组件的页面名称
+    pageName: {
+      type: String,
+      default: ''
+    },
+
+    // tip 信息
+    tip: {
+      type: String,
+      default: ''
+    },
+
+    // 是否显示 "下载模板" 按键
+    hideDownload: {
+      type: Boolean,
+      default: false
+    },
+
+    // 接受的上传文件的类型
+    accept: {
+      type: String,
+      default: '.txt'
+    },
+
+    // 上传文件参数
+    // param: {
+    //   type: Object,
+    //   default: () => ({})
+    // },
+
+    // 下载文件的路径或文件名
+    downloadFile: {
+      type: String,
+      required: true
+    },
+
+    // 上传文件时所需的额外参数
+    uploadData: {
+      type: Object,
+      default: () => ({})
+    },
+
+    // 上传接口
+    url: {
+      type: String,
+      required: true
+    },
+    Ids: {
+      type: String,
+      required: true
+    },
+
+    // 是否是稻香接口
+    isDx: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data () {
+    return {
+      param: { type: 'file' },
+      show: false, // 是否显示弹框
+      UPLOAD_URL: process.env.VUE_APP_UPLOAD_URL, // 上传接口路径
+      uploadHeader: {
+      }, // 请求头内容
+      files: [], // 忆上传的文件
+      loading: false
+    }
+  },
+  computed: {
+    // tempPath () {
+    //   return '?name=' + this.tempUrl + '&version=1.0'
+    // }
+  },
+  watch: {
+    show (val) {
+      // 关闭弹框时清空文件列表
+      if (!val) {
+        this.$refs.import.clearFiles()
+      }
+    }
+  },
+  created () {
+  },
+  mounted () {
+  },
+  methods: {
+    // 显示弹框
+    showModal () {
+      this.show = true
+    },
+
+    // 关闭弹框
+    closeModal () {
+      this.show = false
+    },
+
+    // 检查文件中否是可接受的文件类型
+    checkFile (file, fileList) {
+      const reg = /.*(.txt|.xlsx)$/
+      if (!reg.test(file.name)) {
+        this.$message.error('请上传正确的文件类型')
+        fileList.splice(fileList.indexOf(file), 1)
+      } else if (file.size > K_MAX_SIZE) {
+        this.$message.error('上传文件太大')
+        fileList.splice(fileList.indexOf(file), 1)
+      } else {
+        this.files = fileList.slice(-1)
+      }
+    },
+
+    // 点击"确定"(approval:公众号素材-图文页面是否发起审批的参数 0、不发起审批;1、发起审批)
+    submitUpload (approval) {
+      // 上传文件
+      this.$refs.import.submit()
+    },
+
+    // 上传结果回调
+    uploadResult (args) {
+      this.loading = true
+      if (args.code === 200) {
+        console.log(args)
+        const params = {
+          attachment_id: args.data.id,
+          url: args.data.url,
+          path: args.data.path,
+          storage: args.data.storage,
+          ...this.uploadData
+        }
+        params.ids = this.Ids
+        console.log(params)
+        uploadTemp(this.url, params).then(res => {
+          if (res.code === 200) {
+            // 通知父组件上传完成
+            this.$emit('complete', res.data)
+            this.loading = false
+            this.closeModal()
+          }
+        }).catch(e => {
+          this.$ErrorMessage(e.message, 4000)
+          this.loading = false
+          this.closeModal()
+        })
+      } else {
+        this.$notify.error({
+          title: `上传失败`,
+          message: `详细:\n${args}`,
+          duration: 0
+        })
+        this.loading = false
+        this.closeModal()
+      }
+    },
+
+    // 点击"取消"
+    cancelUpload () {
+      this.$refs.import.clearFiles()
+      this.closeModal()
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+  @import "~@/assets/style/variables.scss";
+
+  .upload-dialog {
+    .import {
+      .el-upload-dragger {
+        width: 540px;
+        //border: dashed 1px $themeColor;
+        background-color: #FAFAFA;
+
+        &:hover {
+          border: dashed 1px $themeColor;
+        }
+      }
+    }
+  }
+</style>
+
+<style scoped lang="scss">
+  @import "~@/assets/style/mixin";
+
+  .import-icon {
+    margin: 25px auto;
+    /*padding-top: 15px;*/
+    width: 60px;
+    height: 60px;
+    line-height: 60px;
+    background-color: #fff;
+    border-radius: 50%;
+    box-shadow: 1px 2px 8px 0px rgba(223, 227, 235, 0.66);
+    transition: all 0.3s;
+    overflow: hidden;
+    @include flex-center;
+
+    &:hover {
+      background-color: #f4f4f4;
+    }
+  }
+
+  .tip {
+    position: relative;
+    text-align: left;
+    font-size: 12px;
+    font-weight: 300;
+    color: rgba(125, 125, 125, 1);
+    line-height: 14px;
+    top: -30px;
+  }
+
+  .btn-wrapper {
+    @include flex-center;
+    margin-bottom: 20px;
+  }
+</style>

+ 68 - 0
src/components/MultiSelectTip.vue

@@ -0,0 +1,68 @@
+<template>
+  <!--操作按钮-->
+  <transition name="el-zoom-in-top">
+    <div v-if="visible" class="operation-btn-container">
+      <c-icon class="icon" icon="icon-common-tip" />
+      <!--提示信息-->
+      <div class="msg">
+        已选择 <span>{{ number }}</span> 项
+      </div>
+      <!--按钮-->
+      <slot />
+    </div>
+  </transition>
+</template>
+
+<script>
+export default {
+  name: 'MultiSelectTip',
+  props: {
+    // 是否显示
+    visible: {
+      type: Boolean,
+      default: false
+    },
+
+    // 已选择的数量
+    number: {
+      type: [Number, String],
+      default: 0
+    }
+  },
+  data () {
+    return {}
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import "~@/assets/style/mixin";
+
+.operation-btn-container {
+  background: rgba(238, 242, 252, 0.8);
+  border: 1px solid $themeColor;
+  border-radius: 2px;
+  @include flex-horizontal-center;
+  margin: 20px 0 10px 0;
+  box-sizing: border-box;
+  padding: 10px 12px;
+
+  .icon {
+    width: 18px;
+    height: 18px;
+  }
+
+  .msg {
+    font-size: 14px;
+    font-family: "PingFang SC";
+    font-weight: 500;
+    color: rgba(144, 147, 153, 1);
+    margin-left: 7px;
+
+    span {
+      color: #000;
+      font-weight: bold;
+    }
+  }
+}
+</style>

+ 184 - 0
src/components/Pagination.vue

@@ -0,0 +1,184 @@
+<template>
+  <div class="clearfix">
+    <el-pagination
+      class="pagination flr"
+      :layout="tools"
+      :current-page.sync="no"
+      :hide-on-single-page="hideOnSinglePage"
+      :page-size.sync="size"
+      :page-sizes="sizes"
+      :total="total"
+      v-bind="$attrs"
+      v-on="$listeners"
+    />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Pagination',
+  props: {
+    // 当前页码
+    pageNo: {
+      type: Number,
+      required: true
+    },
+    // 每页显示多少条
+    pageSize: {
+      type: Number,
+      required: true
+    },
+    // 总数
+    total: {
+      type: Number,
+      required: true
+    },
+    getDataFun: {
+      type: Function,
+      required: true
+    },
+    sizes: {
+      type: Array,
+      default: () => [10, 20, 30, 50, 100]
+    },
+    tools: {
+      type: String,
+      default: 'total, sizes, prev, pager, next'
+    },
+    // 只有一页数据时隐藏分页
+    hideOnSinglePage: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data () {
+    return {
+      size: this.pageSize,
+      no: this.pageNo
+    }
+  },
+  watch: {
+    no (val) {
+      this.$emit('update:pageNo', val)
+      this.getDataFun()
+    },
+
+    size (val) {
+      this.$emit('update:pageSize', val)
+      this.pageNo === 1 ? this.getDataFun() : this.$emit('update:pageNo', 1)
+    },
+
+    pageNo (val) {
+      this.no = val
+    },
+
+    pageSize (val) {
+      this.size = val
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@import "~@/assets/style/variables.scss";
+
+$pageHeight: 24px;
+.pagination {
+  margin: 30px 0px 30px 100px;
+
+  &.el-pagination .btn-prev,
+  &.el-pagination .btn-next {
+    background-color: transparent;
+    height: $pageHeight;
+    line-height: $pageHeight;
+  }
+
+  &.el-pagination span:not([class*="suffix"]),
+  &.el-pagination button {
+    height: $pageHeight;
+    line-height: $pageHeight;
+  }
+
+  .el-button.jump-btn {
+    width: $pageHeight;
+    height: $pageHeight;
+    background-color: $themeColor;
+    border-radius: 50%;
+    text-align: center;
+    line-height: $pageHeight;
+    padding: 0;
+    color: #fff;
+    margin-left: 15px;
+
+    &:hover {
+      background-color: $themeColor;
+    }
+  }
+
+  &.el-pagination__editor.el-input {
+    width: 56px;
+  }
+
+  .el-pagination__editor.el-input .el-input__inner {
+    height: 32px;
+    border-radius: 16px;
+    border: solid 1px #dcdfe6;
+    background-color: #f8f9fb;
+  }
+
+  .el-pager li {
+    min-width: $pageHeight;
+    height: $pageHeight;
+    margin-right: 13px;
+    line-height: $pageHeight;
+    font-size: 14px;
+    color: #474747;
+    font-weight: 500;
+    transition: all 0.3s;
+    border: solid 1px transparent;
+    box-sizing: border-box;
+
+    &:last-child {
+      margin-right: 0;
+    }
+
+    &:hover {
+      border: solid 1px $themeColor;
+      color: $themeColor;
+      box-sizing: border-box;
+      background-color: #fbfafd;
+    }
+
+    &.active {
+      border: solid 1px $themeColor;
+      color: $themeColor;
+      box-sizing: border-box;
+      border-radius: 2px;
+      background-color: #fbfafd;
+
+      &:hover {
+        border: solid 1px $themeColor;
+        color: $themeColor;
+      }
+    }
+  }
+
+  .el-pagination__jump {
+    margin-left: 10px;
+  }
+
+  &.el-pagination .el-pagination__sizes {
+    margin: 0 5px;
+
+    .el-select .el-input {
+      width: 89px;
+
+      .el-input__inner {
+        height: 24px;
+        border-radius: 2px;
+        border: solid 1px #edecf2;
+      }
+    }
+  }
+}
+</style>

+ 122 - 0
src/components/UploadAvatarImg.vue

@@ -0,0 +1,122 @@
+<template>
+  <el-upload
+    class="avatar-uploader"
+    :action="UPLOAD_URL"
+    :headers="uploadHeader"
+    name="iconFile"
+    :show-file-list="false"
+    :before-upload="beforeUpload"
+    :on-progress="uploadProgress"
+    :on-success="uploadSuccess"
+    :on-error="uploadError"
+  >
+    <img v-if="imageUrl" :src="imageUrl" class="avatar">
+    <i v-else class="el-icon-plus avatar-uploader-icon" />
+    <div slot="tip" class="el-upload__tip">支持扩展名:{{ accept }}</div>
+  </el-upload>
+</template>
+
+<script>
+export default {
+  name: 'UploadAvatarImg',
+  props: {
+    size: {
+      type: Number,
+      default: 2097152
+    },
+    accept: {
+      type: String,
+      default: '.jpg,.png,.jpeg'
+    },
+    path: {
+      type: String,
+      default: ''
+    }
+  },
+  data () {
+    return {
+      UPLOAD_URL: process.env.VUE_APP_UPLOAD_URL,
+      uploadHeader: {},
+      imageUrl: ''
+    }
+  },
+  watch: {
+    path: {
+      handler (val) {
+        this.imageUrl = val || ''
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    beforeUpload (file) {
+      if (file.size > this.size) {
+        this.$message.warning('文件大小超出限制')
+        return false
+      }
+      const index = file.name.lastIndexOf('.')
+      const name = file.name.substr(index + 1)
+      if (!this.accept.includes(name)) {
+        this.$message.warning('请上传正确的文件类型')
+        return false
+      }
+    },
+
+    uploadProgress () {
+      this.$emit('uploading')
+    },
+
+    uploadSuccess (args) {
+      if (args.code === 200) {
+        this.imageUrl = args.data
+        this.$emit('upload', args.data)
+      } else {
+        this.imageUrl = ''
+        this.$ErrorMessage(args.msg || '上传失败')
+      }
+    },
+
+    uploadError (err) {
+      console.log(err)
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@import "~@/assets/style/variables.scss";
+.avatar-uploader {
+  .el-upload {
+    border: 1px solid #edecf2;
+    background-color: #fbfafd;
+    border-radius: 3px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+  }
+  .el-upload:hover {
+    border-color: $themeColor;
+    .avatar-uploader-icon {
+      color: $themeColor;
+    }
+  }
+  .avatar-uploader-icon {
+    font-size: 28px;
+    color: #eae9f0;
+    width: 100px;
+    height: 100px;
+    line-height: 100px;
+    text-align: center;
+  }
+  .avatar {
+    width: 100px;
+    height: 100px;
+    display: block;
+  }
+  .el-upload__tip {
+    color: #a8acb4;
+    font-size: 12px;
+    margin-top: -17px;
+  }
+}
+</style>

+ 317 - 0
src/components/UploadImg.vue

@@ -0,0 +1,317 @@
+<template>
+  <div class="universal-upload-container" :style="uStyle">
+    <el-upload
+      name="file"
+      class="universal-upload"
+      :accept="accept"
+      :auto-upload="true"
+      :headers="headers"
+      :data="{type: type}"
+      :before-upload="beforeupload"
+      :on-success="uploadSuccess"
+      :on-error="uploadError"
+      :show-file-list="false"
+      drag
+      :action="UPLOAD_URL"
+    >
+      <img v-if="imgUrl" :src="imgUrl" class="preview-img">
+      <div class="el-upload__text">
+        <i class="el-icon-plus import-icon" />
+      </div>
+    </el-upload>
+    <div v-if="imgUrl && operate" class="preview-img-cover">
+      <span
+        v-if="!disabled"
+        class="delete-btn el-icon-close"
+        @click="handleDelete"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+const K_MAX_SIZE = 2 * 1024 * 1024 // 单位:Byte,文件最大 2M
+export default {
+  name: 'UploadImg',
+  props: {
+    // 接受的上传文件的类型
+    accept: {
+      type: String,
+      default: ''
+    },
+    type: { // 上传额外参数
+      type: String,
+      default: 'image'
+    },
+    previewImg: String,
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    operate: { // 图片上传成功后的操作按钮
+      type: Boolean,
+      default: true
+    },
+    uStyle: { // 改变样式
+      type: Object,
+      default: () => { }
+    }
+  },
+  data () {
+    return {
+      UPLOAD_URL: process.env.VUE_APP_UPLOAD_URL,
+      headers: {},
+      files: [],
+      imgUrl: '',
+      isPreview: false
+    }
+  },
+  watch: {
+    previewImg: {
+      handler (val) {
+        this.imgUrl = val
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    beforeupload (file) {
+      // 检查图片大小和类型
+      if (file.size > K_MAX_SIZE) {
+        this.$WarningMessage('文件大小不能超过2M')
+        return false
+      }
+      const index = file.name.lastIndexOf('.')
+      const name = file.name.substr(index + 1)
+      if (!this.accept.includes(name)) {
+        this.$WarningMessage('请上传正确的文件类型')
+        return false
+      }
+    },
+
+    uploadingprocess () {
+      this.$emit('on-progress')
+    },
+
+    handleDelete () {
+      this.imgUrl = ''
+      this.$emit('on-delete')
+    },
+
+    handleDownload () {
+      var alink = document.createElement('a')
+      alink.href = this.imgUrl
+      alink.download = 'pic' // 图片名
+      alink.target = '_blank' // 图片名
+      alink.click()
+      alink.remove()
+    },
+
+    showPreviewImg () {
+      this.isPreview = true
+    },
+
+    uploadSuccess (res) {
+      if (res.code === 200) {
+        this.imgUrl = res.data || ''
+        this.$emit('on-success', res)
+      } else {
+        this.$ErrorMessage(res.message)
+      }
+    },
+
+    uploadError (res) {
+      this.imgUrl = ''
+      this.$ErrorMessage(res.message)
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.universal-upload {
+  .el-upload {
+    width: 100%;
+    height: 100%;
+  }
+  .el-upload-dragger {
+    width: 100%;
+    height: 100%;
+    border: none;
+    /*border: 1px #d5d5d5 dashed;*/
+    background-color: #f0f4f7;
+    border-radius: 2px;
+    overflow: hidden;
+
+    &:hover {
+      .el-upload__text {
+        color: $themeColor;
+      }
+    }
+    .el-upload__text {
+      position: relative;
+      top: 50%;
+      transform: translateY(-50%);
+      span {
+        font-size: 12px;
+        font-weight: 400;
+        color: #000;
+      }
+      .el-upload__tip {
+        font-size: 10px;
+        font-weight: 300;
+      }
+    }
+  }
+
+  &:hover {
+    .el-upload-dragger {
+      background-color: #ecf1fe;
+    }
+  }
+}
+</style>
+
+<style scoped lang="scss">
+.universal-upload-container {
+  position: relative;
+  width: 100px;
+  height: 100px;
+  transition: border 0.2s;
+  border: solid 1px transparent;
+  box-sizing: border-box;
+  .preview-img-cover {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    background-color: rgba(0, 0, 0, 0.6);
+    text-align: center;
+    box-sizing: border-box;
+    display: none;
+    .delete-btn {
+      width: 17px;
+      height: 17px;
+      line-height: 17px;
+      text-align: center;
+      float: right;
+      color: #fff;
+      font-size: 12px;
+      background-color: rgba(0, 0, 0, 0.6);
+      cursor: pointer;
+      /*width: 17px;*/
+      /*height: 17px;*/
+      /*float: right;*/
+      /*background-color: rgba(0, 0, 0, 0.6);*/
+    }
+    .cover-operate-btn {
+      display: flex;
+      justify-content: space-around;
+      padding-left: 15px;
+      padding-top: 35px;
+      span {
+        color: $themeColor;
+        background-color: #fff;
+        font-size: 18px;
+        display: inline-block;
+        width: 24px;
+        height: 24px;
+        line-height: 24px;
+        border-radius: 50%;
+        text-align: center;
+        margin: 0 5px;
+        cursor: pointer;
+        box-sizing: border-box;
+        &:hover {
+          color: #fff;
+          background-color: $themeColor;
+        }
+      }
+    }
+  }
+  .universal-upload {
+    width: 100%;
+    height: 100%;
+    .preview-img {
+      width: 100%;
+      height: 100%;
+    }
+
+    .import-icon {
+      display: block;
+      font-size: 18px;
+      /*margin: 41px auto;*/
+      color: #939191;
+      font-weight: bold;
+      /*padding-top: 15px;*/
+      /*width: 60px;*/
+      /*height: 60px;*/
+      /*line-height: 75px;*/
+      /*background-color: #fff;*/
+      /*border-radius: 50%;*/
+      /*box-shadow: 1px 2px 8px 0px rgba(223, 227, 235, 0.66);*/
+      /*transition: all 0.3s;*/
+      /*overflow: hidden;*/
+    }
+  }
+
+  &:hover {
+    border: solid 1px $themeColor;
+    border-radius: 2px;
+    box-sizing: border-box;
+    .import-icon {
+      color: $themeColor;
+    }
+    .preview-img-cover {
+      transition: all 0.5s;
+      display: block;
+    }
+  }
+
+  .image-preview-container {
+    position: fixed;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 2000;
+    .image-preview-mask {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      top: 0;
+      left: 0;
+      opacity: 0.5;
+      background: #000;
+    }
+    .image-preview-close {
+      top: 80px;
+      right: 80px;
+      width: 40px;
+      height: 40px;
+      font-size: 40px;
+      cursor: pointer;
+    }
+    .image-preview-btn {
+      position: absolute;
+      z-index: 1;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 50%;
+      opacity: 0.8;
+      cursor: pointer;
+      box-sizing: border-box;
+      user-select: none;
+    }
+    .image-preview-canvas {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+  }
+}
+</style>

+ 118 - 0
src/components/layout/Layout.vue

@@ -0,0 +1,118 @@
+<template>
+  <el-container class="app-layout">
+    <!--在一边-->
+    <el-aside class="aside" style="width: auto !important;">
+      <sidebar />
+    </el-aside>
+    <!--另一边-->
+    <el-container class="container">
+      <!--头-->
+      <el-header class="header" height="100px">
+        <navbar />
+      </el-header>
+      <!--主体-->
+      <el-main class="content" style="padding: 0px;">
+        <el-scrollbar id="mainScroll" class="scroll">
+          <!--页面标签-->
+          <div class="page-tag">
+            {{ $route.meta.title }}
+            <span v-if="$route.meta.subtitle" class="page-tag-subtitle">{{
+              $route.meta.subtitle
+            }}</span>
+          </div>
+          <section class="page-container">
+            <transition name="fade-transform" mode="out-in">
+              <router-view :key="key" />
+            </transition>
+          </section>
+        </el-scrollbar>
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script>
+import SideBar from './SideBar'
+import NavBar from './NavBar'
+
+export default {
+  name: 'Layout',
+  components: {
+    sidebar: SideBar,
+    navbar: NavBar
+  },
+  data() {
+    return {}
+  },
+  computed: {
+    key() {
+      return this.$route.fullPath
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.app-layout {
+  .scroll {
+    z-index: 4;
+  }
+}
+</style>
+
+<style scoped lang="scss">
+.app-layout {
+  width: 100%;
+  height: 100vh;
+  min-width: 1360px;
+
+  .aside {
+    position: relative;
+    z-index: 9;
+    background-color: #efeff4;
+
+    // 去掉滚动条
+    &::-webkit-scrollbar {
+      width: 0;
+      background-color: transparent;
+    }
+  }
+
+  .header {
+    background-color: #fff;
+    padding: 0;
+    z-index: 10;
+  }
+  .container {
+    overflow: hidden;
+    z-index: 8;
+    .content {
+      position: relative;
+      z-index: 4;
+      background-color: #fff;
+      padding: 34px 0 0 0;
+
+      .scroll {
+        box-sizing: border-box;
+
+        .page-container {
+          box-sizing: border-box;
+          padding: 0 40px;
+        }
+      }
+    }
+  }
+}
+
+.page-tag {
+  color: #333a3f;
+  font-size: 24px;
+  font-weight: bold;
+  margin: 26px 40px 20px 40px;
+  .page-tag-subtitle {
+    font-size: 12px;
+    color: #878787;
+    font-weight: 400;
+  }
+}
+</style>

+ 219 - 0
src/components/layout/NavBar.vue

@@ -0,0 +1,219 @@
+<template>
+  <div class="navbar-box">
+    <div class="navbar-search">
+      <el-input v-model="searchContent" placeholder="Search...">
+        <template slot="prepend">
+          <div class="icon" @click="onClickSearch" />
+        </template>
+      </el-input>
+    </div>
+    <div class="navbar-right">
+      <el-badge :value="0" :hidden="true" :max="99" class="item">
+        <i class="icon-notification el-icon-message-solid" />
+      </el-badge>
+
+      <!--头像-->
+      <div class="avatar-wrapper">
+        <div class="image-box">
+          <img v-if="userInfo.avatar" :src="userInfo.avatar" alt="">
+          <img
+            v-else
+            :src="require('@/assets/image/icon/icon-default-avatar.png')"
+            alt=""
+          >
+        </div>
+      </div>
+      <!--用户名-->
+      <el-dropdown>
+        <div class="user-info">
+          <span class="name">{{ userInfo.username }}</span>
+          <span class="status"><i class="dot" /> 在线</span>
+        </div>
+        <i class="el-icon-arrow-down icon-right" />
+        <el-dropdown-menu>
+          <el-dropdown-item :command="0">
+            <s-confirm-box
+              type="warning"
+              text="确定要退出登录吗?"
+              content=""
+              @confirm="handleLogout"
+            >
+              <span class="logout">退出登录</span>
+            </s-confirm-box>
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { logout } from '@/network/api/api-base'
+import ConfirmBox from '@/components/ConfirmBox'
+
+export default {
+  name: 'NavBar',
+  components: {
+    's-confirm-box': ConfirmBox
+  },
+  data () {
+    return {
+      searchContent: ''
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo', 'isCollapse'])
+  },
+  methods: {
+    // 搜索
+    onClickSearch () {},
+
+    // 退出登录
+    handleLogout () {
+      logout().then((res) => {
+        if (res.code === 200) {
+          const _url = window.location.origin
+          window.location.href = res.data + '?redirect_url=' + _url
+        }
+      }).catch((e) => {
+        this.$ErrorMessage(e.msg || '')
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.navbar-search {
+  .el-input {
+    height: 40px;
+    width: 360px;
+
+    input {
+      width: 300px;
+      background: transparent;
+      border: 0;
+      -webkit-appearance: none;
+      border-radius: 0px;
+      padding: 5px 10px;
+      color: #000;
+      height: 40px;
+      font-size: 18px;
+      font-weight: 300;
+    }
+
+    .el-input-group__prepend {
+      background-color: transparent;
+      border: none;
+    }
+  }
+}
+
+.navbar-right {
+  .el-badge__content {
+    padding: 0 4.5px;
+    height: 14px;
+    line-height: 14px;
+    background-color: #fb4444;
+  }
+}
+</style>
+
+<style scoped lang="scss">
+@import "~@/assets/style/mixin.scss";
+
+.navbar-box {
+  display: flex;
+  justify-content: space-between;
+  height: 96px;
+  background-color: #fff;
+  box-shadow: 0px 2px 4px 0px rgba(230, 231, 236, 0.4);
+
+  .navbar-search {
+    height: 100%;
+    padding: 0 15px;
+    @include flex-center;
+    cursor: pointer;
+
+    .icon {
+      width: 20px;
+      height: 20px;
+      background-size: 20px !important;
+      background: url(~@/assets/image/icon/icon-nav-search-normal.png) 0 0
+        no-repeat;
+
+      &:hover {
+        background: url(~@/assets/image/icon/icon-nav-search-hover.png) 0 0
+          no-repeat;
+      }
+    }
+  }
+
+  .navbar-right {
+    margin-top: 20px;
+    margin-right: 50px;
+
+    .avatar-wrapper {
+      display: inline-block;
+      margin-left: 50px;
+      margin-right: 10px;
+      position: relative;
+      top: 5px;
+
+      .image-box {
+        width: 46px;
+        height: 46px;
+        border-radius: 50%;
+        overflow: hidden;
+
+        img {
+          display: block;
+          width: 100%;
+          height: 100%;
+        }
+      }
+    }
+  }
+
+  .icon-notification {
+    color: #b7b9c5;
+    font-size: 24px;
+    position: relative;
+    top: -10px;
+  }
+
+  .user-info {
+    display: inline-block;
+    cursor: pointer;
+    user-select: none;
+
+    .name {
+      font-size: 16px;
+      color: #212937;
+      display: block;
+    }
+
+    .status {
+      color: #91949f;
+      font-size: 12px;
+
+      .dot {
+        display: inline-block;
+        background-color: #6adbaa;
+        width: 8px;
+        height: 8px;
+        border-radius: 50%;
+      }
+    }
+  }
+
+  .icon-right {
+    position: relative;
+    top: -10px;
+    color: #555a6f;
+    font-size: 10px;
+    margin-left: 15px;
+  }
+}
+</style>

+ 124 - 0
src/components/layout/SideBar.vue

@@ -0,0 +1,124 @@
+<template>
+  <el-scrollbar class="scroll">
+    <div class="system-logo">
+      <div class="system-icon" @click="onClickMenu">
+        <img src="@/assets/image/system-book.png" alt="">
+      </div>
+      <div v-if="!isCollapse" class="system-info">
+        <p>
+          投放平台 <i class="icon-menu el-icon-s-fold" @click="onClickMenu" />
+        </p>
+        <span>version: _V1.0 </span>
+      </div>
+    </div>
+
+    <el-menu
+      :collapse="isCollapse"
+      mode="vertical"
+      :default-active="$route.path"
+      :default-openeds="['/novel-manage', '/tag-manage', '/data-config', '/user-manage', '/sign', '/system', '/project/goods', '/order', '/activity']"
+      router
+      class="menu"
+      :style="{ width: isCollapse ? '88px' : '240px' }"
+    >
+      <sidebar-item v-for="(item, index) in menu" :key="index" :item="item" />
+    </el-menu>
+  </el-scrollbar>
+</template>
+
+<script>
+import SidebarItem from './SidebarItem'
+import { mapGetters, mapMutations } from 'vuex'
+
+export default {
+  name: 'SideBar',
+  components: {
+    SidebarItem
+  },
+  data () {
+    return {}
+  },
+  computed: {
+    ...mapGetters(['isCollapse', 'menu'])
+  },
+  methods: {
+    ...mapMutations('user', ['TOGGLE_MENU', 'SET_MENU']),
+    // 点击菜单按钮
+
+    onClickMenu () {
+      this.TOGGLE_MENU(!this.isCollapse)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import "~@/assets/style/mixin.scss";
+
+.menu {
+  position: relative;
+  width: 240px;
+  height: calc(100% - 150px);
+  box-sizing: border-box;
+  background-color: #efeff4;
+  display: flex;
+  flex-direction: column;
+  font-size: 14px;
+  cursor: pointer;
+  transition: width 200ms;
+}
+
+.system-logo {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 33px;
+  margin-bottom: 20px;
+  .system-icon {
+    width: 39px;
+    height: 39px;
+    margin-right: 8px;
+    cursor: pointer;
+
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  .icon-menu {
+    color: #555a6f;
+    font-size: 16px;
+    margin-left: 50px;
+    cursor: pointer;
+  }
+
+  .system-info {
+    text-align: left;
+
+    p {
+      font-size: 18px;
+      color: #212937;
+      font-weight: 400;
+      margin: 0;
+      white-space: nowrap;
+    }
+
+    span {
+      font-size: 10px;
+      color: #91949f;
+      position: relative;
+      top: -5px;
+    }
+  }
+}
+
+@keyframes fadeOut {
+  0% {
+    opacity: 1;
+  }
+  100% {
+    opacity: 0;
+  }
+}
+</style>

+ 179 - 0
src/components/layout/SidebarItem.vue

@@ -0,0 +1,179 @@
+<template>
+  <el-submenu v-if="item.children" :index="item.url">
+    <template slot="title">
+      <i
+        class="icon-menu-img"
+        :style="{ backgroundImage: `url(${item.icon})` }"
+      />
+      <span>{{ item.menuName }}</span>
+    </template>
+    <sidebar-item v-for="(child, i) in item.children" :key="i" :item="child" />
+  </el-submenu>
+  <el-menu-item v-else :index="item.url">
+    <i
+      v-if="item.icon"
+      class="icon-menu-img"
+      :style="{ backgroundImage: `url(${item.icon})` }"
+    />
+    <span>{{ item.menuName }}</span>
+  </el-menu-item>
+</template>
+
+<script>
+export default {
+  name: 'SidebarItem',
+  props: {
+    item: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {}
+  }
+}
+</script>
+
+<style lang="scss">
+.el-tooltip__popper.is-dark .icon {
+  width: 30px;
+  height: 30px;
+  margin-right: 18px;
+}
+
+.menu.el-menu {
+  .el-menu {
+    background-color: #efeff4;
+  }
+
+  & > .el-menu-item,
+  .el-submenu__title {
+    height: 42px;
+    line-height: 42px;
+    padding-bottom: 10px;
+  }
+
+  .el-menu-item,
+  .el-submenu__title {
+    color: #555a6f;
+    font-size: 14px;
+  }
+
+  .icon {
+    margin-right: 11px;
+    margin-left: 10px;
+    font-size: 18px;
+  }
+
+  .icon-dot {
+    display: inline-block;
+    width: 4px;
+    height: 4px;
+    border-radius: 50%;
+    background-color: #409EFF;
+    margin-right: 11px;
+  }
+
+  .icon-menu-img {
+    width: 16px;
+    height: 16px;
+    display: inline-block;
+    background-repeat: no-repeat;
+    background-size: cover;
+    padding: 0;
+    margin-right: 11px;
+    margin-left: 10px;
+    background-position-x: left;
+  }
+
+  .el-submenu .el-menu-item {
+    padding-left: 60px !important;
+    //margin-left: 20px !important;
+  }
+
+  .el-menu-item.is-active,
+  .el-submenu.is-active {
+    background-color: #fff;
+
+    &:focus {
+      background-color: #fff;
+    }
+
+    &:hover {
+      background-color: #fff;
+    }
+
+    .icon-menu-img {
+      background-position-x: right;
+    }
+
+    .el-submenu__title {
+      color: #409EFF;
+    }
+  }
+
+  & > .el-menu-item:hover,
+  .el-submenu__title:hover {
+    background-color: inherit;
+    color: #409EFF;
+
+    .icon {
+      color: #409EFF;
+    }
+
+    &::after {
+      content: "";
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      top: 0;
+      background-color: transparent;
+    }
+
+    .icon-menu-img {
+      background-position-x: right;
+    }
+  }
+
+  .el-submenu.is-active .el-submenu__title {
+    background-color: #efeff4;
+  }
+
+  .el-menu.el-menu--inline {
+    .el-menu-item {
+      &:focus {
+        background-color: transparent;
+      }
+
+      &:hover {
+        background-color: transparent;
+        color: #409EFF;
+        transition: transform 0.1s linear;
+      }
+
+      &.is-active {
+        color: #409EFF;
+        font-weight: bold;
+        padding-left: 40px !important;
+        background-color: #fff;
+        margin-right: -2px;
+
+        &:before {
+          content: "";
+          display: inline-block;
+          width: 4px;
+          height: 4px;
+          border-radius: 50%;
+          background-color: #409EFF;
+          margin-right: 16px;
+        }
+
+        .icon-menu-img {
+          background-position-x: right;
+        }
+      }
+    }
+  }
+}
+</style>

+ 31 - 0
src/main.js

@@ -0,0 +1,31 @@
+import Vue from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import Element from 'element-ui'
+
+import GlobalPlugin from './common/plugins'
+
+import '@/assets/style/index.scss'
+
+import './router/permission'
+
+import has from '@/common/directive/permission/'
+
+import { hasPerm } from '@/common/utils/has-permission'
+
+Vue.config.productionTip = false
+
+Vue.directive('has', has)
+
+Vue.prototype.hasPerm = hasPerm
+
+Vue.use(Element, { size: 'medium' })
+// 全局方法挂载
+Vue.use(GlobalPlugin)
+
+new Vue({
+  router,
+  store,
+  render: h => h(App)
+}).$mount('#app')

+ 18 - 0
src/network/api/adminLog.js

@@ -0,0 +1,18 @@
+import service from '@/network/service/service'
+
+export function Page(data) {
+  return service.post('/admin/adminLog/page', data)
+}
+
+export function Add(data) {
+  return service.post('/admin/adminLog/save', data)
+}
+
+export function Update(data) {
+  return service.post('/admin/adminLog/update', data)
+}
+
+export function Delete(data) {
+  return service.post('/admin/adminLog/delete', data)
+}
+

+ 39 - 0
src/network/api/api-base.js

@@ -0,0 +1,39 @@
+import service from '@/network/service/service'
+
+/**
+ * 获取用户信息
+ */
+export function getUserInfo() {
+  return service.get('/system/admin/getAdminInfo')
+}
+
+/**
+ * 获取菜单
+ */
+export function getMenuList(params) {
+  return service.get('/system/admin/getMenuAndRole', { params })
+}
+
+/**
+ * 退出登录
+ */
+export function logout(params) {
+  return service.get('/system//admin/logout', { params })
+}
+
+/**
+ * @description 文件上传
+ * @param params 文件流
+ */
+export function uploadFile(params) {
+  return service.post(process.env.VUE_APP_UPLOAD_URL, params)
+}
+
+/**
+ * @description 上传模板(超时设置为 30秒)
+ * @param path 上传接口路径
+ * @param params 接口参数
+ */
+export function uploadTemp(path, params) {
+  return service.post(`/api${path}`, params, { timeout: 30000 })
+}

+ 22 - 0
src/network/api/dict.js

@@ -0,0 +1,22 @@
+import service from '@/network/service/service'
+
+export function QueryByGroup(params) {
+  return service.get('/admin/dict/queryByGroup', { params })
+}
+
+export function Page(data) {
+  return service.post('/admin/dict/page', data)
+}
+
+export function Add(data) {
+  return service.post('/admin/dict/save', data)
+}
+
+export function Update(data) {
+  return service.post('/admin/dict/update', data)
+}
+
+export function Delete(data) {
+  return service.post('/admin/dict/delete', data)
+}
+

+ 17 - 0
src/network/api/system.js

@@ -0,0 +1,17 @@
+import service from '@/network/service/service'
+
+export function exec(data) {
+  return service.post('/admin/redis/exec', data)
+}
+
+export function uploadFile(params) {
+  return service({
+    method: 'post',
+    url: '/admin/file/upload',
+    data: params,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+}
+

+ 81 - 0
src/network/service/service.js

@@ -0,0 +1,81 @@
+import axios from 'axios'
+import { MessageBox, Message } from 'element-ui'
+
+const service = axios.create({
+  timeout: 30000
+})
+
+// 发起请求前的处理
+service.interceptors.request.use(
+  (config) => {
+    config.headers['Cache-Control'] = 'no-cache'
+    return config
+  },
+  (error) => {
+    return Promise.reject(error)
+  }
+)
+service.defaults.withCredentials = true
+
+// 收到响应的处理
+service.interceptors.response.use(
+  (response) => {
+    if (response.headers['content-disposition']) {
+      return response
+    }
+    if (response.status === 200) {
+      if (response.data.code === 200) {
+        return response.data
+      } else {
+        Message({
+          type: 'error',
+          message: response.data.msg,
+          showClose: true
+        })
+      }
+    } else {
+      return Promise.reject(response.data)
+    }
+  },
+  (error) => {
+    if (
+      error.code === 'ECONNABORTED' &&
+      error.message.indexOf('timeout') > -1
+    ) {
+      return Promise.reject(new Error('请求超时'))
+    }
+    const err = error.response.data || {}
+    const status = error.response.status
+    let errMsg = ''
+    if (err.errors) {
+      const errArr = []
+      for (const k in err.errors) {
+        errArr.push(err.errors[k].toString())
+      }
+      errMsg = errArr.toString()
+    } else {
+      errMsg = err.msg || '获取失败'
+    }
+    switch (status) {
+      case 401:
+        MessageBox.alert('认证失效,请重新登录', '', {
+          confirmButtonText: '确定',
+          type: 'warning',
+          showClose: false,
+          center: true,
+          callback: () => {
+            const _url = window.location.origin
+            window.location.href = error.response.data.loginPageUrl + '?redirect_url=' + _url
+          }
+        })
+        break
+      default:
+        if (!errMsg) {
+          errMsg = '当前网络环境较差,请刷新重试'
+        }
+        return Promise.reject({ msg: errMsg })
+    }
+  }
+)
+
+export default service

+ 59 - 0
src/router/index.js

@@ -0,0 +1,59 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+
+import Layout from '@/components/layout/Layout'
+
+Vue.use(Router)
+
+const viewRoutes = [{
+  path: '/',
+  component: Layout,
+  redirect: 'home',
+  children: [{
+    path: 'home',
+    name: 'home',
+    meta: { title: '首页' },
+    component: () =>
+      import('@/views/home/Home')
+  },
+  {
+    path: '/adminLog',
+    name: 'adminLog',
+    meta: { title: '操作日志' },
+    component: () =>
+      import('@/views/adminLog')
+  },
+  {
+    path: '/redis',
+    name: 'redis',
+    meta: { title: 'RedisManager' },
+    component: () =>
+      import('@/views/redis')
+  },
+  {
+    path: '/dict',
+    name: 'dict',
+    meta: { title: '字典' },
+    component: () =>
+      import('@/views/dict')
+  }
+  ]
+},
+{
+  path: '*',
+  name: 'err404',
+  component: () =>
+    import('@/components/Err404')
+}
+]
+
+const createRouter = () =>
+  new Router({
+    mode: 'history', // 去掉url中的#
+    scrollBehavior: () => ({ y: 0 }),
+    routes: viewRoutes
+  })
+
+const router = createRouter()
+
+export default router

+ 22 - 0
src/router/permission.js

@@ -0,0 +1,22 @@
+import router from './index'
+import store from '../store'
+
+const whiteList = ['/']
+
+router.beforeEach(async (to, from, next) => {
+  if (whiteList.indexOf(to.path) !== -1) {
+    next()
+  } else {
+    if (Object.keys(store.getters.userInfo).length > 0) {
+      next()
+    } else {
+      try {
+        await store.dispatch('user/GetUserInfo')
+        await store.dispatch('user/GetUserMenu')
+        next()
+      } catch (error) {
+        next()
+      }
+    }
+  }
+})

+ 22 - 0
src/store/index.js

@@ -0,0 +1,22 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import user from './modules/store-user'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: {
+  },
+  getters: {
+    userInfo: state => state.user.userInfo,
+    menu: state => state.user.menu,
+    isCollapse: state => state.user.isCollapse
+  },
+  mutations: {
+  },
+  actions: {},
+  modules: {
+    user: user
+  }
+})

+ 32 - 0
src/store/modules/menu.js

@@ -0,0 +1,32 @@
+export const menus = [
+  {
+    icon:
+      'https://test.moka-space.mokamrp.com/202105/Lark20210525-155715.png',
+    path: '/system',
+    menuId: 1,
+    menuName: '系统管理',
+    url: '/system',
+    children: [
+      {
+        icon: '',
+        path: '/dict',
+        menuId: 1,
+        menuName: '字典管理',
+        url: '/dict'
+      }, {
+        icon: '',
+        path: '/adminLog',
+        menuId: 1,
+        menuName: '操作日志',
+        url: '/adminLog'
+      },
+      {
+        icon: '',
+        path: '/redis',
+        menuId: 2,
+        menuName: 'RedisManager',
+        url: '/redis'
+      }
+    ]
+  }
+]

+ 72 - 0
src/store/modules/store-user.js

@@ -0,0 +1,72 @@
+import { getUserInfo, getMenuList } from '@/network/api/api-base'
+
+export default {
+  namespaced: true,
+  state: {
+    userInfo: {},
+    permission: [],
+    isCollapse: false,
+    menu: []
+  },
+  mutations: {
+    SET_USER_INFO: (state, info) => {
+      state.userInfo = info
+    },
+    SET_USER_PERMISSION: (state, permission) => {
+      const _permission = []
+      permission.map((v) => {
+        if (v.path) {
+          _permission.push(v.path)
+        }
+      })
+      state.permission = _permission
+    },
+    CLEAR(state) {
+      state.userInfo = {}
+      state.permission = []
+      state.isCollapse = false
+      state.menu = []
+    },
+    SET_MENU(state, menu) {
+      state.menu = menu
+    },
+    TOGGLE_MENU(state, isCollapse) {
+      state.isCollapse = isCollapse
+    }
+  },
+  actions: {
+    // 获取用户信息
+    async GetUserInfo({ commit, rootMutations }) {
+      try {
+        const res = await getUserInfo()
+        if (res.code === 200) {
+          commit('SET_USER_INFO', {
+            avatar: res.data.plugininfo['pic'] || '',
+            username: res.data.username,
+            userid: res.data.userid
+          })
+          // resolve()
+        }
+      } catch (error) {
+        // reject(error)
+      }
+    },
+    // 获取用户菜单
+    async GetUserMenu({ commit, rootMutations }) {
+      try {
+        const res = await getMenuList()
+        if (res.code === 200) {
+          commit('SET_MENU', res.data.menu || [])
+          commit('SET_USER_PERMISSION', res.data.permission || [])
+          // resolve()
+        } else {
+          commit('SET_MENU', [])
+        }
+      } catch (error) {
+        // reject(error)
+        console.log(error)
+        // commit('SET_MENU', menus)
+      }
+    }
+  }
+}

BIN
src/views/.DS_Store


+ 109 - 0
src/views/adminLog/AdminLogMain.vue

@@ -0,0 +1,109 @@
+<template>
+  <div>
+    <Search ref="search" />
+    <el-table v-loading="loading" stripe class="list-table" :data="data">
+      <c-data-empty slot="empty" />
+      <el-table-column prop="operator" label="操作人" width="300" align="center">
+        <template slot-scope="{row}">
+          {{ `${row.operator}(${row.operatorId})` }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="ip" label="IP" width="300" align="center" />
+      <el-table-column prop="uri" label="uri" width="300" align="center" />
+      <el-table-column prop="parameter" show-overflow-tooltip label="请求参数" width="300" align="center" />
+      <el-table-column prop="payload" show-overflow-tooltip label="请求参数" align="center" />
+      <el-table-column prop="createTime" label="操作时间" width="300" align="center" />
+      <el-table-column prop="partitionField" label="日期分片字段" width="300" align="center" />
+    </el-table>
+    <c-pagination
+      :page-size.sync="pageInfo.pageSize"
+      :page-no.sync="pageInfo.pageNum"
+      :total="pageInfo.total"
+      :get-data-fun="loadData"
+    />
+    <OptionDialog ref="dialog" />
+  </div>
+</template>
+
+<script>
+import { PageCommonComponent } from '@/common/mixin'
+import {
+  Page,
+  Delete
+} from '@/network/api/adminLog'
+import Search from './Search'
+import OptionDialog from './OptionDialog'
+import { AdminLogEnt } from './object'
+
+export default {
+  name: 'AdminLog',
+  components: { Search, OptionDialog },
+  mixins: [PageCommonComponent],
+  provide: function () {
+    return {
+      loadData: this.loadData,
+      showEdit: this.showEdit
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      data: [],
+      pageInfo: {
+        pageNum: 1,
+        pageSize: 10,
+        total: 0
+      }
+    }
+  },
+  mounted() {
+    this.loadData()
+  },
+  methods: {
+    loadData(isFirst = false) {
+      const searchForm = this.$refs.search.getSearchForm()
+      this.doLoadData(isFirst, searchForm)
+    },
+    doLoadData(firstPage = false, searchForm) {
+      firstPage && (this.pageInfo.pageNum = 1)
+      const req = { ...searchForm, ...this.pageInfo }
+      this.loading = true
+      Page(req).then(res => {
+        this.data = res.data.records
+        this.pageInfo.total = res.data.total
+        this.loading = false
+      })
+    },
+    deleteRow(row) {
+      this.$confirm('确定删除, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        Delete({ id: row.id }).then(res => {
+          this.$message({
+            message: '删除成功',
+            type: 'success'
+          })
+          this.loadData()
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    showEdit(row) {
+      this.$refs.dialog.show(true, { ...row })
+    },
+    showAdd() {
+      this.$refs.dialog.show(false, new AdminLogEnt())
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 116 - 0
src/views/adminLog/OptionDialog.vue

@@ -0,0 +1,116 @@
+<template>
+  <el-dialog
+    :title="isEdit ? '编 辑': '新 增'"
+    width="45%"
+    :visible.sync="visible"
+    :before-close="close"
+    label-position="right"
+    :modal-append-to-body="false"
+    :close-on-press-escape="false"
+    :close-on-click-modal="false"
+  >
+    <el-form ref="ent" :model="ent" :rules="rules" label-width="110px">
+      <el-form-item label="ID" prop="id">
+        <el-input v-model="ent.id" />
+      </el-form-item>
+      <el-form-item label="操作人" prop="operator">
+        <el-input v-model="ent.operator" />
+      </el-form-item>
+      <el-form-item label="操作人ID" prop="operatorId">
+        <el-input v-model="ent.operatorId" />
+      </el-form-item>
+      <el-form-item label="IP" prop="ip">
+        <el-input v-model="ent.ip" />
+      </el-form-item>
+      <el-form-item label="uri" prop="uri">
+        <el-input v-model="ent.uri" />
+      </el-form-item>
+      <el-form-item label="请求参数" prop="parameter">
+        <el-input v-model="ent.parameter" />
+      </el-form-item>
+      <el-form-item label="请求参数" prop="payload">
+        <el-input v-model="ent.payload" />
+      </el-form-item>
+      <el-form-item label="日期分片字段" prop="partitionField">
+        <el-input v-model="ent.partitionField" />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-input v-model="ent.createTime" />
+      </el-form-item>
+    </el-form>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="close">取消</el-button>
+      <el-button type="primary" @click="submit">确定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+import {
+  Add,
+  Update
+} from '@/network/api/adminLog'
+import { AdminLogEnt, formRules } from './object'
+
+export default {
+  inject: ['loadData'],
+  data() {
+    return {
+      ent: new AdminLogEnt(),
+      rules: formRules,
+      isEdit: false,
+      visible: false,
+      cityList: []
+    }
+  },
+  methods: {
+    somethingFunction() {
+      console.log('xxxx')
+    },
+    submit() {
+      this.$refs.ent.validate((validate) => {
+        if (validate) {
+          this.isEdit ? this.updateRow() : this.addRow()
+        }
+      })
+    },
+    addRow() {
+      const req = { ...this.ent }
+      Add(req).then(res => {
+        this.$message({
+          message: '保存成功',
+          type: 'success'
+        })
+        this.loadData()
+        this.close()
+      })
+    },
+    updateRow() {
+      const req = { ...this.ent }
+      Update(req).then(res => {
+        this.$message({
+          message: '修改成功',
+          type: 'success'
+        })
+        this.loadData()
+        this.close()
+      })
+    },
+    close() {
+      this.visible = false
+      this.$nextTick(() => {
+        this.$refs.ent.resetFields()
+      })
+    },
+    show(isEdit = false, ent) {
+      this.ent = ent
+      this.isEdit = isEdit
+      this.visible = true
+      this.$nextTick(() => {
+        if (this.isEdit) {
+          console.log()
+        }
+      })
+    }
+  }
+}
+</script>

+ 77 - 0
src/views/adminLog/Search.vue

@@ -0,0 +1,77 @@
+<template>
+  <el-form ref="searchForm" class="search-form" :model="searchForm">
+    <el-row :gutter="12">
+      <el-col :span="6">
+        <el-form-item prop="operator">
+          <el-input v-model="searchForm.operator" placeholder="操作人姓名" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="6">
+        <el-form-item prop="operatorId">
+          <el-input v-model="searchForm.operatorId" placeholder="操作人ID" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="6">
+        <el-form-item prop="uri">
+          <el-input v-model="searchForm.uri" placeholder="uri" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="6">
+        <el-form-item prop="createTime">
+          <el-date-picker
+            v-model="searchForm.createTime"
+            type="datetimerange"
+            range-separator="至"
+            start-placeholder="开始时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            end-placeholder="结束时间"
+            @change="handleTimeChange"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+    <el-row style="padding-bottom: 10px">
+      <el-col :span="6">
+        <el-button class="btn" type="primary" @click="search">
+          查询
+        </el-button>
+        <el-button class="btn" @click="resetSearch">
+          重置
+        </el-button>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+<script>
+import { searchForm } from './object'
+
+export default {
+  inject: ['loadData'],
+  data() {
+    return {
+      searchForm: searchForm
+    }
+  },
+  methods: {
+    search() {
+      this.loadData(true)
+    },
+    // 折中方案
+    getSearchForm() {
+      return { ...this.searchForm }
+    },
+    handleTimeChange(timeRange) {
+      if (timeRange.length === 2) {
+        this.searchForm.startTime = timeRange[0]
+        this.searchForm.endTime = timeRange[1]
+      } else {
+        this.searchForm.startTime = null
+        this.searchForm.endTime = null
+      }
+    },
+    resetSearch() {
+      this.$refs.searchForm.resetFields()
+    }
+  }
+}
+</script>

+ 3 - 0
src/views/adminLog/index.js

@@ -0,0 +1,3 @@
+import AdminLog from './AdminLogMain.vue'
+
+export default AdminLog

+ 27 - 0
src/views/adminLog/object.js

@@ -0,0 +1,27 @@
+export const searchForm = {
+  id: null,
+  operator: null,
+  operatorId: null,
+  ip: null,
+  uri: null,
+  parameter: null,
+  payload: null,
+  createTime: [],
+  startTime: null,
+  endTime: null
+}
+
+export class AdminLogEnt {
+  id
+  operator
+  operatorId
+  ip
+  uri
+  parameter
+  payload
+  partitionField
+  createTime
+}
+
+export const formRules = {
+}

+ 115 - 0
src/views/dict/DictMain.vue

@@ -0,0 +1,115 @@
+<template>
+  <div>
+    <Search ref="search" />
+    <el-row type="flex" justify="space-between" class="operate-box">
+      <el-button type="primary" icon="el-icon-plus" round @click="showAdd">新 增</el-button>
+    </el-row>
+    <div class="table-container">
+      <el-table v-loading="loading" stripe class="list-table" :data="data">
+        <c-data-empty slot="empty" />
+        <el-table-column label="操作" width="100">
+          <template slot-scope="{row}">
+            <el-button type="text" size="small" @click="showEdit(row)">编辑</el-button>
+            <el-button type="text" size="small" @click="deleteRow(row)">删除</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column prop="groupCode" label="组" align="center" />
+        <el-table-column prop="keyCode" label="KEY" align="center" />
+        <el-table-column prop="value" label="Value" align="center" />
+        <el-table-column prop="remark" label="说明" align="center" />
+        <el-table-column prop="createBy" label="操作人" width="100" align="center" />
+        <el-table-column prop="createTime" label="创建时间" width="200" align="center" />
+      </el-table>
+    </div>
+    <c-pagination
+      :page-size.sync="pageInfo.pageSize"
+      :page-no.sync="pageInfo.pageNum"
+      :total="pageInfo.total"
+      :get-data-fun="loadData"
+    />
+    <OptionDialog ref="dialog" />
+  </div>
+</template>
+
+<script>
+import { PageCommonComponent } from '@/common/mixin'
+import {
+  Page,
+  Delete
+} from '@/network/api/dict'
+import Search from './Search'
+import OptionDialog from './OptionDialog'
+import { DictEnt } from './object'
+
+export default {
+  name: 'Dict',
+  components: { Search, OptionDialog },
+  mixins: [PageCommonComponent],
+  provide: function () {
+    return {
+      loadData: this.loadData,
+      showEdit: this.showEdit
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      data: [],
+      pageInfo: {
+        pageNum: 1,
+        pageSize: 10,
+        total: 0
+      }
+    }
+  },
+  mounted() {
+    this.loadData()
+  },
+  methods: {
+    loadData(isFirst = false) {
+      const searchForm = this.$refs.search.getSearchForm()
+      this.doLoadData(isFirst, searchForm)
+    },
+    doLoadData(firstPage = false, searchForm) {
+      firstPage && (this.pageInfo.pageNum = 1)
+      const req = { ...searchForm, ...this.pageInfo }
+      this.loading = true
+      Page(req).then(res => {
+        this.data = res.data.records
+        this.pageInfo.total = res.data.total
+        this.loading = false
+      })
+    },
+    deleteRow(row) {
+      this.$confirm('确定删除, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        Delete({ id: row.id }).then(res => {
+          this.$message({
+            message: '删除成功',
+            type: 'success'
+          })
+          this.loadData()
+        })
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    showEdit(row) {
+      this.$refs.dialog.show(true, { ...row })
+    },
+    showAdd() {
+      this.$refs.dialog.show(false, new DictEnt())
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 101 - 0
src/views/dict/OptionDialog.vue

@@ -0,0 +1,101 @@
+<template>
+  <el-dialog
+    :title="isEdit ? '编 辑': '新 增'"
+    width="45%"
+    :visible.sync="visible"
+    :before-close="close"
+    label-position="right"
+    :modal-append-to-body="false"
+    :close-on-press-escape="false"
+    :close-on-click-modal="false"
+  >
+    <el-form ref="ent" :model="ent" :rules="rules" label-width="110px">
+      <el-form-item label="组" prop="groupCode">
+        <el-input v-model="ent.groupCode" />
+      </el-form-item>
+      <el-form-item label="KEY" prop="keyCode">
+        <el-input v-model="ent.keyCode" />
+      </el-form-item>
+      <el-form-item label="Value" prop="value">
+        <el-input v-model="ent.value" />
+      </el-form-item>
+      <el-form-item label="说明" prop="remark">
+        <el-input v-model="ent.remark" />
+      </el-form-item>
+    </el-form>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="close">取消</el-button>
+      <el-button type="primary" @click="submit">确定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+import {
+  Add,
+  Update
+} from '@/network/api/dict'
+import { DictEnt, formRules } from './object'
+
+export default {
+  inject: ['loadData'],
+  data() {
+    return {
+      ent: new DictEnt(),
+      rules: formRules,
+      isEdit: false,
+      visible: false,
+      cityList: []
+    }
+  },
+  methods: {
+    somethingFunction() {
+      console.log('xxxx')
+    },
+    submit() {
+      this.$refs.ent.validate((validate) => {
+        if (validate) {
+          this.isEdit ? this.updateRow() : this.addRow()
+        }
+      })
+    },
+    addRow() {
+      const req = { ...this.ent }
+      Add(req).then(res => {
+        this.$message({
+          message: '保存成功',
+          type: 'success'
+        })
+        this.loadData()
+        this.close()
+      })
+    },
+    updateRow() {
+      const req = { ...this.ent }
+      Update(req).then(res => {
+        this.$message({
+          message: '修改成功',
+          type: 'success'
+        })
+        this.loadData()
+        this.close()
+      })
+    },
+    close() {
+      this.visible = false
+      this.$nextTick(() => {
+        this.$refs.ent.resetFields()
+      })
+    },
+    show(isEdit = false, ent) {
+      this.ent = ent
+      this.isEdit = isEdit
+      this.visible = true
+      this.$nextTick(() => {
+        if (this.isEdit) {
+          console.log()
+        }
+      })
+    }
+  }
+}
+</script>

+ 53 - 0
src/views/dict/Search.vue

@@ -0,0 +1,53 @@
+<template>
+  <el-form ref="searchForm" class="search-form" :model="searchForm">
+    <el-row :gutter="12">
+      <el-col :span="6">
+        <el-form-item prop="groupCode">
+          <el-input v-model="searchForm.groupCode" placeholder="组" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="6">
+        <el-form-item prop="keyCode">
+          <el-input v-model="searchForm.keyCode" placeholder="KEY" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="6">
+        <el-form-item prop="value">
+          <el-input v-model="searchForm.value" placeholder="Value" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="6">
+        <el-button class="btn" type="primary" @click="search">
+          查询
+        </el-button>
+        <el-button class="btn" @click="resetSearch">
+          重置
+        </el-button>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+<script>
+import { searchForm } from './object'
+
+export default {
+  inject: ['loadData'],
+  data() {
+    return {
+      searchForm: searchForm
+    }
+  },
+  methods: {
+    search() {
+      this.loadData(true)
+    },
+    // 折中方案
+    getSearchForm() {
+      return { ...this.searchForm }
+    },
+    resetSearch() {
+      this.$refs.searchForm.resetFields()
+    }
+  }
+}
+</script>

+ 3 - 0
src/views/dict/index.js

@@ -0,0 +1,3 @@
+import Dict from './DictMain.vue'
+
+export default Dict

+ 23 - 0
src/views/dict/object.js

@@ -0,0 +1,23 @@
+export const searchForm = {
+  groupCode: null,
+  keyCode: null,
+  value: null
+}
+
+export class DictEnt {
+  id
+  groupCode
+  keyCode
+  value
+  remark
+  createBy
+  createTime
+  updateBy
+  updateTime
+}
+
+export const formRules = {
+  groupCode: [{ required: true, message: '请填写组', trigger: 'change' }],
+  keyCode: [{ required: true, message: '请填写KEY', trigger: 'change' }],
+  value: [{ required: true, message: '请填写Value', trigger: 'change' }]
+}

+ 12 - 0
src/views/home/Home.vue

@@ -0,0 +1,12 @@
+<template>
+  <div class="home" />
+</template>
+
+<script>
+export default {
+  name: 'Home'
+}
+</script>
+
+<style scoped lang="scss">
+</style>

+ 3 - 0
src/views/redis/index.js

@@ -0,0 +1,3 @@
+import redisManager from './redisManager.vue'
+
+export default redisManager

+ 67 - 0
src/views/redis/redisManager.vue

@@ -0,0 +1,67 @@
+<template>
+  <el-row>
+    <el-col :span="12">
+      <el-form ref="redisForm" label-width="100" :model="redisCommand">
+        <el-form-item label="Command" prop="command">
+          <el-select v-model="redisCommand.command" style="width: 100%;">
+            <el-option v-for="item in commandList" :key="item" :value="item">{{ item }}</el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="Key" prop="key">
+          <el-input v-model="redisCommand.key" placeholder="Key" />
+        </el-form-item>
+        <el-form-item label="HKey" prop="value">
+          <el-input v-model="redisCommand.hkey" placeholder="HKey" />
+        </el-form-item>
+        <el-form-item label="Value" prop="value">
+          <el-input v-model="redisCommand.value" placeholder="Value" type="textarea" :rows="10" />
+        </el-form-item>
+        <el-form-item style="text-align: right">
+          <el-button class="btn" type="primary" :loading="isBtnLoading" round @click="exec">执行</el-button>
+          <el-button class="btn" round @click="reset">重置</el-button>
+        </el-form-item>
+        <el-form-item label="Result" prop="result">
+          <el-input v-model="result" placeholder="Result" type="textarea" :rows="10" />
+        </el-form-item>
+      </el-form>
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+import { exec } from '@/network/api/system'
+
+export default {
+  name: 'Redis',
+  data() {
+    this.commandList = ['get', 'set', 'del', 'hget', 'hset', 'hdel']
+    return {
+      isBtnLoading: false,
+      result: '',
+      redisCommand: {
+        'command': '',
+        'key': '',
+        'hkey': '',
+        'value': ''
+      }
+    }
+  },
+  methods: {
+    exec() {
+      this.isBtnLoading = true
+      exec(this.redisCommand).then(res => {
+        this.isBtnLoading = false
+        this.result = JSON.stringify(res.data, null, '  ')
+      })
+    },
+    reset() {
+      this.$refs.redisForm.resetFields()
+      this.result = ''
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

Некоторые файлы не были показаны из-за большого количества измененных файлов