guhongwei 4 years ago
commit
3292c89892
89 changed files with 6107 additions and 0 deletions
  1. 2 0
      .env
  2. 33 0
      .eslintrc.js
  3. 23 0
      .gitignore
  4. 0 0
      .prettierrc
  5. 4 0
      README.md
  6. 3 0
      babel.config.js
  7. 61 0
      package.json
  8. 17 0
      public/index.html
  9. 10 0
      src/App.vue
  10. 28 0
      src/assets/css/color-dark.css
  11. 4 0
      src/assets/css/icon.css
  12. 177 0
      src/assets/css/main.css
  13. 29 0
      src/assets/css/theme-green/color-green.css
  14. BIN
      src/assets/css/theme-green/fonts/element-icons.ttf
  15. BIN
      src/assets/css/theme-green/fonts/element-icons.woff
  16. 1 0
      src/assets/css/theme-green/index.css
  17. BIN
      src/assets/img/img.jpg
  18. BIN
      src/assets/img/login-bg.jpg
  19. BIN
      src/assets/logo.png
  20. 171 0
      src/components/common/Header.vue
  21. 51 0
      src/components/common/Home.vue
  22. 276 0
      src/components/common/Sidebar.vue
  23. 181 0
      src/components/common/Tags.vue
  24. 6 0
      src/components/common/bus.js
  25. 79 0
      src/components/common/directives.js
  26. 30 0
      src/components/common/i18n.js
  27. 50 0
      src/components/frame/filter-page-table.md
  28. 299 0
      src/components/frame/filter-page-table.vue
  29. 82 0
      src/components/frame/form.md
  30. 211 0
      src/components/frame/form.vue
  31. 11 0
      src/components/frame/pagination.md
  32. 51 0
      src/components/frame/pagination.vue
  33. 54 0
      src/components/frame/qrcode.vue
  34. 76 0
      src/components/frame/uploadone.vue
  35. 77 0
      src/components/frame/wang-editor.vue
  36. 55 0
      src/components/page/403.vue
  37. 55 0
      src/components/page/404.vue
  38. 95 0
      src/components/page/Dashboard.vue
  39. 112 0
      src/components/page/Login.vue
  40. 209 0
      src/components/page/branch/index.vue
  41. 245 0
      src/components/page/business/index.vue
  42. 141 0
      src/components/page/fhSign/fhsjdeta.vue
  43. 146 0
      src/components/page/fhSign/fhxsdeta.vue
  44. 139 0
      src/components/page/fhSign/fhxsstat.vue
  45. 170 0
      src/components/page/homeParts/fhstat.vue
  46. 119 0
      src/components/page/homeParts/sjstat.vue
  47. 121 0
      src/components/page/homeParts/zhstat.vue
  48. 210 0
      src/components/page/peer/index.vue
  49. 136 0
      src/components/page/sale/index.vue
  50. 84 0
      src/components/page/setting/hdimage.vue
  51. 67 0
      src/components/page/setting/personal.vue
  52. 66 0
      src/components/page/setting/qrcode.vue
  53. 63 0
      src/components/page/setting/updatepwd.vue
  54. 146 0
      src/components/page/sjSign/sjxsdeta.vue
  55. 144 0
      src/components/page/zhSign/fhSign.vue
  56. 142 0
      src/components/page/zhSign/sjSign.vue
  57. 124 0
      src/components/page/zhSign/weixin.vue
  58. 49 0
      src/main.js
  59. 19 0
      src/plugins/axios.js
  60. 39 0
      src/plugins/check-res.js
  61. 5 0
      src/plugins/element.js
  62. 6 0
      src/plugins/filters.js
  63. 27 0
      src/plugins/loading.js
  64. 4 0
      src/plugins/meta.js
  65. 33 0
      src/plugins/methods.js
  66. 21 0
      src/plugins/setting.js
  67. 65 0
      src/plugins/stomp.js
  68. 25 0
      src/plugins/var.js
  69. 118 0
      src/router/index.js
  70. 30 0
      src/store/count.js
  71. 34 0
      src/store/index.js
  72. 43 0
      src/store/kh.js
  73. 47 0
      src/store/login.js
  74. 43 0
      src/store/password.js
  75. 22 0
      src/store/select/khselect.js
  76. 43 0
      src/store/sj.js
  77. 43 0
      src/store/user.js
  78. 33 0
      src/store/user/mutations.js
  79. 2 0
      src/store/user/state.js
  80. 43 0
      src/store/xs.js
  81. 43 0
      src/store/xt.js
  82. 26 0
      src/store/zf.js
  83. 117 0
      src/util/axios-wrapper.js
  84. 10 0
      src/util/filters.js
  85. 30 0
      src/util/menus.js
  86. 50 0
      src/util/methods-util.js
  87. 47 0
      src/util/optionTitles.js
  88. 69 0
      src/util/user-util.js
  89. 35 0
      vue.config.js

+ 2 - 0
.env

@@ -0,0 +1,2 @@
+VUE_APP_AXIOS_BASE_URL = ''
+VUE_APP_ROUTER="/ccb"

+ 33 - 0
.eslintrc.js

@@ -0,0 +1,33 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  env: {
+    node: true,
+  },
+  extends: ['plugin:vue/essential', '@vue/prettier'],
+  plugins: ['vue'],
+  rules: {
+    'max-len': [
+      'warn',
+      {
+        code: 250,
+      },
+    ],
+    'no-unused-vars': 'off',
+    'no-console': 'off',
+    'prettier/prettier': [
+      'warn',
+      {
+        singleQuote: true,
+        trailingComma: 'es5',
+        bracketSpacing: true,
+        jsxBracketSameLine: true,
+        printWidth: 160,
+      },
+    ],
+  },
+  parserOptions: {
+    parser: 'babel-eslint',
+  },
+};

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+package-lock.json
+/dist
+example.html
+favicon.ico
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw*

+ 0 - 0
.prettierrc


+ 4 - 0
README.md

@@ -0,0 +1,4 @@
+## 项目操作
+### 下载依赖包 npm i &&  npm install
+### 运行项目 npm run serve
+### 打包项目 npm run build

+ 3 - 0
babel.config.js

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

+ 61 - 0
package.json

@@ -0,0 +1,61 @@
+{
+  "name": "pc-adminsys",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "axios": "^0.20.0",
+    "babel-polyfill": "^6.26.0",
+    "core-js": "^3.6.5",
+    "element-ui": "^2.13.2",
+    "lodash": "^4.17.20",
+    "naf-core": "^0.1.2",
+    "qrcode": "^1.4.4",
+    "vue": "^2.6.11",
+    "vue-i18n": "^8.21.0",
+    "vue-meta": "^2.4.0",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0",
+    "wangeditor": "^3.1.1"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-plugin-router": "~4.5.0",
+    "@vue/cli-plugin-vuex": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^3.1.3",
+    "eslint-plugin-vue": "^6.2.2",
+    "less": "^3.0.4",
+    "less-loader": "^5.0.0",
+    "prettier": "^1.19.1",
+    "vue-template-compiler": "^2.6.11"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended",
+      "@vue/prettier"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

+ 17 - 0
public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+  <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><%= htmlWebpackPlugin.options.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>

+ 10 - 0
src/App.vue

@@ -0,0 +1,10 @@
+<template>
+  <div id="app">
+    <router-view></router-view>
+  </div>
+</template>
+<style>
+@import './assets/css/main.css';
+@import './assets/css/color-dark.css'; /*深色主题*/
+/*@import "./assets/css/theme-green/color-green.css";   浅绿色主题*/
+</style>

+ 28 - 0
src/assets/css/color-dark.css

@@ -0,0 +1,28 @@
+.header{
+    background-color: #242f42;
+}
+.login-wrap{
+    background: #324157;
+}
+.plugins-tips{
+    background: #eef1f6;
+}
+.plugins-tips a{
+    color: #20a0ff;
+}
+.el-upload--text em {
+    color: #20a0ff;
+}
+.pure-button{
+    background: #20a0ff;
+}
+.tags-li.active {
+    border: 1px solid #409EFF;
+    background-color: #409EFF;
+}
+.message-title{
+    color: #20a0ff;
+}
+.collapse-btn:hover{
+    background: rgb(40,52,70);
+}

+ 4 - 0
src/assets/css/icon.css

@@ -0,0 +1,4 @@
+
+    [class*=" el-icon-lx"], [class^=el-icon-lx] {
+        font-family: lx-iconfont!important;
+    }

+ 177 - 0
src/assets/css/main.css

@@ -0,0 +1,177 @@
+* {
+    margin: 0;
+    padding: 0;
+}
+
+html,
+body,
+#app,
+.wrapper {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+}
+
+body {
+    font-family: 'PingFang SC', "Helvetica Neue", Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif;
+}
+
+a {
+    text-decoration: none
+}
+
+
+.content-box {
+    position: absolute;
+    left: 250px;
+    right: 0;
+    top: 70px;
+    bottom: 0;
+    padding-bottom: 30px;
+    -webkit-transition: left .3s ease-in-out;
+    transition: left .3s ease-in-out;
+    background: #f0f0f0;
+}
+
+.content {
+    width: auto;
+    height: 100%;
+    padding: 10px;
+    overflow-y: scroll;
+    box-sizing: border-box;
+}
+
+.content-collapse {
+    left: 65px;
+}
+
+.container {
+    padding: 30px;
+    background: #fff;
+    border: 1px solid #ddd;
+    border-radius: 5px;
+}
+
+.crumbs {
+    margin: 10px 0;
+}
+
+.el-table th {
+    background-color: #f5f7fa !important;
+}
+
+.pagination {
+    margin: 20px 0;
+    text-align: right;
+}
+
+.plugins-tips {
+    padding: 20px 10px;
+    margin-bottom: 20px;
+}
+
+.el-button+.el-tooltip {
+    margin-left: 10px;
+}
+
+.el-table tr:hover {
+    background: #f6faff;
+}
+
+.mgb20 {
+    margin-bottom: 20px;
+}
+
+.move-enter-active,
+.move-leave-active {
+    transition: opacity .5s;
+}
+
+.move-enter,
+.move-leave {
+    opacity: 0;
+}
+
+/*BaseForm*/
+
+.form-box {
+    width: 600px;
+}
+
+.form-box .line {
+    text-align: center;
+}
+
+.el-time-panel__content::after,
+.el-time-panel__content::before {
+    margin-top: -7px;
+}
+
+.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) {
+    padding-bottom: 0;
+}
+
+/*Upload*/
+
+.pure-button {
+    width: 150px;
+    height: 40px;
+    line-height: 40px;
+    text-align: center;
+    color: #fff;
+    border-radius: 3px;
+}
+
+.g-core-image-corp-container .info-aside {
+    height: 45px;
+}
+
+.el-upload--text {
+    background-color: #fff;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    box-sizing: border-box;
+    width: 360px;
+    height: 180px;
+    text-align: center;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+}
+
+.el-upload--text .el-icon-upload {
+    font-size: 67px;
+    color: #97a8be;
+    margin: 40px 0 16px;
+    line-height: 50px;
+}
+
+.el-upload--text {
+    color: #97a8be;
+    font-size: 14px;
+    text-align: center;
+}
+
+.el-upload--text em {
+    font-style: normal;
+}
+
+/*VueEditor*/
+
+.ql-container {
+    min-height: 400px;
+}
+
+.ql-snow .ql-tooltip {
+    transform: translateX(117.5px) translateY(10px) !important;
+}
+
+.editor-btn {
+    margin-top: 20px;
+}
+
+/*markdown*/
+
+.v-note-wrapper .v-note-panel {
+    min-height: 500px;
+}

+ 29 - 0
src/assets/css/theme-green/color-green.css

@@ -0,0 +1,29 @@
+.header{
+    background-color: #07c4a8;
+}
+.login-wrap{
+    background: rgba(56, 157, 170, 0.82);;
+}
+.plugins-tips{
+    background: #f2f2f2;
+}
+.plugins-tips a{
+    color: #00d1b2;
+}
+.el-upload--text em {
+    color: #00d1b2;
+}
+.pure-button{
+    background: #00d1b2;
+}
+.pagination > .active > a, .pagination > .active > a:hover, .pagination > .active > a:focus, .pagination > .active > span, .pagination > .active > span:hover, .pagination > .active > span:focus {
+    background-color: #00d1b2 !important;
+    border-color: #00d1b2 !important;
+}
+.tags-li.active {
+    border: 1px solid #00d1b2;
+    background-color: #00d1b2;
+}
+.collapse-btn:hover{
+    background: #00d1b2;
+}

BIN
src/assets/css/theme-green/fonts/element-icons.ttf


BIN
src/assets/css/theme-green/fonts/element-icons.woff


File diff suppressed because it is too large
+ 1 - 0
src/assets/css/theme-green/index.css


BIN
src/assets/img/img.jpg


BIN
src/assets/img/login-bg.jpg


BIN
src/assets/logo.png


+ 171 - 0
src/components/common/Header.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="header">
+    <!-- 折叠按钮 -->
+    <div class="collapse-btn" @click="collapseChage">
+      <i v-if="!collapse" class="el-icon-s-fold"></i>
+      <i v-else class="el-icon-s-unfold"></i>
+    </div>
+    <div class="logo">后台管理系统</div>
+    <div class="header-right">
+      <div class="header-user-con">
+        <!-- 全屏显示 -->
+        <div class="btn-fullscreen" @click="handleFullScreen">
+          <el-tooltip effect="dark" :content="fullscreen ? `取消全屏` : `全屏`" placement="bottom">
+            <i class="el-icon-rank"></i>
+          </el-tooltip>
+        </div>
+        <!-- 用户头像 -->
+        <div class="user-avator">
+          <img src="../../assets/img/img.jpg" />
+        </div>
+        <!-- 用户名下拉菜单 -->
+        <el-dropdown class="user-name" trigger="click" @command="handleCommand">
+          <span class="el-dropdown-link">
+            {{ user.name }}
+            <i class="el-icon-caret-bottom"></i>
+          </span>
+          <el-dropdown-menu slot="dropdown">
+            <el-dropdown-item divided command="loginout">退出登录</el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import bus from '../common/bus';
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  data() {
+    return {
+      collapse: false,
+      fullscreen: false,
+    };
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+  methods: {
+    // 用户名下拉菜单选择事件
+    handleCommand(command) {
+      if (command == 'loginout') {
+        localStorage.removeItem('user');
+        this.$router.push('/login');
+      }
+    },
+    // 侧边栏折叠
+    collapseChage() {
+      this.collapse = !this.collapse;
+      bus.$emit('collapse', this.collapse);
+    },
+    // 全屏事件
+    handleFullScreen() {
+      let element = document.documentElement;
+      if (this.fullscreen) {
+        if (document.exitFullscreen) {
+          document.exitFullscreen();
+        } else if (document.webkitCancelFullScreen) {
+          document.webkitCancelFullScreen();
+        } else if (document.mozCancelFullScreen) {
+          document.mozCancelFullScreen();
+        } else if (document.msExitFullscreen) {
+          document.msExitFullscreen();
+        }
+      } else {
+        if (element.requestFullscreen) {
+          element.requestFullscreen();
+        } else if (element.webkitRequestFullScreen) {
+          element.webkitRequestFullScreen();
+        } else if (element.mozRequestFullScreen) {
+          element.mozRequestFullScreen();
+        } else if (element.msRequestFullscreen) {
+          // IE11
+          element.msRequestFullscreen();
+        }
+      }
+      this.fullscreen = !this.fullscreen;
+    },
+  },
+  mounted() {
+    if (document.body.clientWidth < 1500) {
+      this.collapseChage();
+    }
+  },
+};
+</script>
+<style scoped>
+.header {
+  position: relative;
+  box-sizing: border-box;
+  width: 100%;
+  height: 70px;
+  font-size: 22px;
+  color: #fff;
+}
+.collapse-btn {
+  float: left;
+  padding: 0 21px;
+  cursor: pointer;
+  line-height: 70px;
+}
+.header .logo {
+  float: left;
+  width: 250px;
+  line-height: 70px;
+}
+.header-right {
+  float: right;
+  padding-right: 50px;
+}
+.header-user-con {
+  display: flex;
+  height: 70px;
+  align-items: center;
+}
+.btn-fullscreen {
+  transform: rotate(45deg);
+  margin-right: 5px;
+  font-size: 24px;
+}
+.btn-bell,
+.btn-fullscreen {
+  position: relative;
+  width: 30px;
+  height: 30px;
+  text-align: center;
+  border-radius: 15px;
+  cursor: pointer;
+}
+.btn-bell-badge {
+  position: absolute;
+  right: 0;
+  top: -2px;
+  width: 8px;
+  height: 8px;
+  border-radius: 4px;
+  background: #f56c6c;
+  color: #fff;
+}
+.btn-bell .el-icon-bell {
+  color: #fff;
+}
+.user-name {
+  margin-left: 10px;
+}
+.user-avator {
+  margin-left: 20px;
+}
+.user-avator img {
+  display: block;
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+}
+.el-dropdown-link {
+  color: #fff;
+  cursor: pointer;
+}
+.el-dropdown-menu__item {
+  text-align: center;
+}
+</style>

+ 51 - 0
src/components/common/Home.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="wrapper">
+    <v-head></v-head>
+    <v-sidebar></v-sidebar>
+    <div class="content-box" :class="{ 'content-collapse': collapse }">
+      <v-tags></v-tags>
+      <div class="content">
+        <transition name="move" mode="out-in">
+          <keep-alive :include="tagsList">
+            <router-view></router-view>
+          </keep-alive>
+        </transition>
+        <el-backtop target=".content"></el-backtop>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import vHead from './Header.vue';
+import vSidebar from './Sidebar.vue';
+import vTags from './Tags.vue';
+import bus from './bus';
+export default {
+  data() {
+    return {
+      tagsList: [],
+      collapse: false,
+    };
+  },
+  components: {
+    vHead,
+    vSidebar,
+    vTags,
+  },
+  created() {
+    bus.$on('collapse-content', msg => {
+      this.collapse = msg;
+    });
+
+    // 只有在标签页列表里的页面才使用keep-alive,即关闭标签之后就不保存到内存中了。
+    bus.$on('tags', msg => {
+      let arr = [];
+      for (let i = 0, len = msg.length; i < len; i++) {
+        msg[i].name && arr.push(msg[i].name);
+      }
+      this.tagsList = arr;
+    });
+  },
+};
+</script>

+ 276 - 0
src/components/common/Sidebar.vue

@@ -0,0 +1,276 @@
+<template>
+  <div class="sidebar">
+    <el-menu
+      class="sidebar-el-menu"
+      :default-active="onRoutes"
+      :collapse="collapse"
+      background-color="#324157"
+      text-color="#bfcbd9"
+      active-text-color="#20a0ff"
+      unique-opened
+      router
+    >
+      <template v-for="item in items">
+        <template v-if="item.subs">
+          <el-submenu class="second" :index="item.index" :key="item.index">
+            <template slot="title">
+              <i :class="item.icon"></i>
+              <span slot="title">{{ item.title }}</span>
+            </template>
+            <template v-for="subItem in item.subs">
+              <el-submenu v-if="subItem.subs" :index="subItem.index" :key="subItem.index">
+                <template slot="title">{{ subItem.title }}</template>
+                <el-menu-item v-for="(threeItem, i) in subItem.subs" :key="i" :index="threeItem.index">{{ threeItem.title }}</el-menu-item>
+              </el-submenu>
+              <el-menu-item v-else :index="subItem.index" :key="subItem.index">{{ subItem.title }}</el-menu-item>
+            </template>
+          </el-submenu>
+        </template>
+        <template v-else>
+          <el-menu-item class="first" :index="item.index" :key="item.index">
+            <i :class="item.icon"></i>
+            <span slot="title">{{ item.title }}</span>
+          </el-menu-item>
+        </template>
+      </template>
+    </el-menu>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import { mapState, createNamespacedHelpers } from 'vuex';
+import bus from '../common/bus';
+export default {
+  data() {
+    return {
+      collapse: false,
+      items: [
+        {
+          icon: 'el-icon-s-home',
+          index: 'dashboard',
+          title: '系统首页',
+        },
+      ],
+    };
+  },
+  computed: {
+    ...mapState(['user']),
+    onRoutes() {
+      return this.$route.path.replace('/', '');
+    },
+  },
+  created() {
+    // 通过 Event Bus 进行组件间通信,来折叠侧边栏
+    bus.$on('collapse', msg => {
+      this.collapse = msg;
+      bus.$emit('collapse-content', msg);
+    });
+  },
+  methods: {
+    // 分配用户彩带权限
+    getMenu() {
+      // 用户信息
+      let user = this.user;
+      // 复制列表
+      let list = _.cloneDeep(this.items);
+      // 用户为省行
+      if (user.js == '0') {
+        let data = [
+          {
+            icon: 'el-icon-copy-document',
+            index: 'peer',
+            title: '同级账号管理',
+          },
+          {
+            icon: 'el-icon-user-solid',
+            index: 'branch',
+            title: '分行用户管理',
+          },
+          {
+            icon: 'el-icon-setting',
+            index: '2',
+            title: '系统设置',
+            subs: [
+              // {
+              //   index: 'personal',
+              //   title: '个人信息',
+              // },
+              {
+                index: 'updatepwd',
+                title: '修改登录密码',
+              },
+              {
+                index: 'qrcode',
+                title: '二维码查看',
+              },
+              {
+                index: 'hdimage',
+                title: '活动设置图',
+              },
+            ],
+          },
+          {
+            icon: 'el-icon-pie-chart',
+            index: '3',
+            title: '报名统计',
+            subs: [
+              {
+                index: 'fhSign',
+                title: '分行客户报名统计',
+              },
+              {
+                index: 'sjSign',
+                title: '商家客户报名统计',
+              },
+              {
+                index: 'weixin',
+                title: '微信宣传转发统计',
+              },
+            ],
+          },
+        ];
+        list.push(...data);
+        this.$set(this, `items`, _.uniqBy(list, 'index'));
+        // 用户为分行
+      } else if (user.js == '1') {
+        let data = [
+          {
+            icon: 'el-icon-user-solid',
+            index: 'business',
+            title: '商家用户管理',
+          },
+          {
+            icon: 'el-icon-s-custom',
+            index: 'sale',
+            title: '销售员管理',
+          },
+          {
+            icon: 'el-icon-setting',
+            index: '2',
+            title: '系统设置',
+            subs: [
+              // {
+              //   index: 'personal',
+              //   title: '个人信息',
+              // },
+              {
+                index: 'updatepwd',
+                title: '修改登录密码',
+              },
+              {
+                index: 'qrcode',
+                title: '二维码查看',
+              },
+            ],
+          },
+          {
+            icon: 'el-icon-pie-chart',
+            index: '3',
+            title: '报名统计',
+            subs: [
+              {
+                index: 'fhxsdeta',
+                title: '销售员业绩明细',
+              },
+              {
+                index: 'fhxsstat',
+                title: '销售员业绩统计',
+              },
+              {
+                index: 'fhsjdeta',
+                title: '商家报名统计',
+              },
+              {
+                index: 'weixin',
+                title: '微信宣传转发统计',
+              },
+            ],
+          },
+        ];
+        list.push(...data);
+        this.$set(this, `items`, _.uniqBy(list, 'index'));
+      } else if (user.js == '2') {
+        let data = [
+          {
+            icon: 'el-icon-s-custom',
+            index: 'sale',
+            title: '销售员管理',
+          },
+          {
+            icon: 'el-icon-setting',
+            index: '2',
+            title: '系统设置',
+            subs: [
+              // {
+              //   index: 'personal',
+              //   title: '个人信息',
+              // },
+              {
+                index: 'updatepwd',
+                title: '修改登录密码',
+              },
+              {
+                index: 'qrcode',
+                title: '二维码查看',
+              },
+            ],
+          },
+          {
+            icon: 'el-icon-s-home',
+            index: '3',
+            title: '报名统计',
+            subs: [
+              {
+                index: 'sjxsdeta',
+                title: '销售员业绩明细',
+              },
+              {
+                index: 'weixin',
+                title: '微信宣传转发统计',
+              },
+            ],
+          },
+        ];
+        list.push(...data);
+        this.$set(this, `items`, _.uniqBy(list, 'index'));
+      }
+    },
+  },
+  watch: {
+    user: {
+      deep: true,
+      immediate: true,
+      handler(val) {
+        this.getMenu();
+      },
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.sidebar {
+  display: block;
+  position: absolute;
+  left: 0;
+  top: 70px;
+  bottom: 0;
+  overflow-y: scroll;
+}
+.sidebar::-webkit-scrollbar {
+  width: 0;
+}
+.sidebar-el-menu:not(.el-menu--collapse) {
+  width: 250px;
+}
+.sidebar > ul {
+  height: 100%;
+}
+// /deep/.first {
+//   padding-left: 0 !important;
+// }
+// /deep/.second .el-submenu__title {
+//   padding-left: 0 !important;
+// }
+</style>

+ 181 - 0
src/components/common/Tags.vue

@@ -0,0 +1,181 @@
+<template>
+  <div class="tags" v-if="showTags">
+    <ul>
+      <li class="tags-li" v-for="(item, index) in tagsList" :class="{ active: isActive(item.path) }" :key="index">
+        <router-link :to="item.path" class="tags-li-title">
+          {{ item.title }}
+        </router-link>
+        <span class="tags-li-icon" @click="closeTags(index)"><i class="el-icon-close"></i></span>
+      </li>
+    </ul>
+    <div class="tags-close-box">
+      <el-dropdown @command="handleTags">
+        <el-button size="mini" type="primary"> 标签选项<i class="el-icon-arrow-down el-icon--right"></i> </el-button>
+        <el-dropdown-menu size="small" slot="dropdown">
+          <el-dropdown-item command="other">关闭其他</el-dropdown-item>
+          <el-dropdown-item command="all">关闭所有</el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<script>
+import bus from './bus';
+export default {
+  data() {
+    return {
+      tagsList: [],
+    };
+  },
+  methods: {
+    isActive(path) {
+      return path === this.$route.fullPath;
+    },
+    // 关闭单个标签
+    closeTags(index) {
+      const delItem = this.tagsList.splice(index, 1)[0];
+      const item = this.tagsList[index] ? this.tagsList[index] : this.tagsList[index - 1];
+      if (item) {
+        delItem.path === this.$route.fullPath && this.$router.push(item.path);
+      } else {
+        this.$router.push('/');
+      }
+    },
+    // 关闭全部标签
+    closeAll() {
+      this.tagsList = [];
+      this.$router.push('/');
+    },
+    // 关闭其他标签
+    closeOther() {
+      const curItem = this.tagsList.filter(item => {
+        return item.path === this.$route.fullPath;
+      });
+      this.tagsList = curItem;
+    },
+    // 设置标签
+    setTags(route) {
+      const isExist = this.tagsList.some(item => {
+        return item.path === route.fullPath;
+      });
+      if (!isExist) {
+        if (this.tagsList.length >= 8) {
+          this.tagsList.shift();
+        }
+        this.tagsList.push({
+          title: route.meta.title,
+          path: route.fullPath,
+          name: route.matched[1].components.default.name,
+        });
+      }
+      bus.$emit('tags', this.tagsList);
+    },
+    handleTags(command) {
+      command === 'other' ? this.closeOther() : this.closeAll();
+    },
+  },
+  computed: {
+    showTags() {
+      return this.tagsList.length > 0;
+    },
+  },
+  watch: {
+    $route(newValue, oldValue) {
+      this.setTags(newValue);
+    },
+  },
+  created() {
+    this.setTags(this.$route);
+    // 监听关闭当前页面的标签页
+    bus.$on('close_current_tags', () => {
+      for (let i = 0, len = this.tagsList.length; i < len; i++) {
+        const item = this.tagsList[i];
+        if (item.path === this.$route.fullPath) {
+          if (i < len - 1) {
+            this.$router.push(this.tagsList[i + 1].path);
+          } else if (i > 0) {
+            this.$router.push(this.tagsList[i - 1].path);
+          } else {
+            this.$router.push('/');
+          }
+          this.tagsList.splice(i, 1);
+          break;
+        }
+      }
+    });
+  },
+};
+</script>
+
+<style>
+.tags {
+  position: relative;
+  height: 30px;
+  overflow: hidden;
+  background: #fff;
+  padding-right: 120px;
+  box-shadow: 0 5px 10px #ddd;
+}
+
+.tags ul {
+  box-sizing: border-box;
+  width: 100%;
+  height: 100%;
+}
+
+.tags-li {
+  float: left;
+  margin: 3px 5px 2px 3px;
+  border-radius: 3px;
+  font-size: 12px;
+  overflow: hidden;
+  cursor: pointer;
+  height: 23px;
+  line-height: 23px;
+  border: 1px solid #e9eaec;
+  background: #fff;
+  padding: 0 5px 0 12px;
+  vertical-align: middle;
+  color: #666;
+  -webkit-transition: all 0.3s ease-in;
+  -moz-transition: all 0.3s ease-in;
+  transition: all 0.3s ease-in;
+}
+
+.tags-li:not(.active):hover {
+  background: #f8f8f8;
+}
+
+.tags-li.active {
+  color: #fff;
+}
+
+.tags-li-title {
+  float: left;
+  max-width: 80px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  margin-right: 5px;
+  color: #666;
+}
+
+.tags-li.active .tags-li-title {
+  color: #fff;
+}
+
+.tags-close-box {
+  position: absolute;
+  right: 0;
+  top: 0;
+  box-sizing: border-box;
+  padding-top: 1px;
+  text-align: center;
+  width: 110px;
+  height: 30px;
+  background: #fff;
+  box-shadow: -3px 0 15px 3px rgba(0, 0, 0, 0.1);
+  z-index: 10;
+}
+</style>

+ 6 - 0
src/components/common/bus.js

@@ -0,0 +1,6 @@
+import Vue from 'vue';
+
+// 使用 Event Bus
+const bus = new Vue();
+
+export default bus;

+ 79 - 0
src/components/common/directives.js

@@ -0,0 +1,79 @@
+import Vue from 'vue';
+
+// v-dialogDrag: 弹窗拖拽属性
+Vue.directive('dialogDrag', {
+  bind(el, binding, vnode, oldVnode) {
+    const dialogHeaderEl = el.querySelector('.el-dialog__header');
+    const dragDom = el.querySelector('.el-dialog');
+
+    dialogHeaderEl.style.cssText += ';cursor:move;';
+    dragDom.style.cssText += ';top:0px;';
+
+    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
+    const sty = (() => {
+      if (window.document.currentStyle) {
+        return (dom, attr) => dom.currentStyle[attr];
+      } else {
+        return (dom, attr) => getComputedStyle(dom, false)[attr];
+      }
+    })();
+
+    dialogHeaderEl.onmousedown = e => {
+      // 鼠标按下,计算当前元素距离可视区的距离
+      const disX = e.clientX - dialogHeaderEl.offsetLeft;
+      const disY = e.clientY - dialogHeaderEl.offsetTop;
+
+      const screenWidth = document.body.clientWidth; // body当前宽度
+      const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
+
+      const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
+      const dragDomheight = dragDom.offsetHeight; // 对话框高度
+
+      const minDragDomLeft = dragDom.offsetLeft;
+      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
+
+      const minDragDomTop = dragDom.offsetTop;
+      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
+
+      // 获取到的值带px 正则匹配替换
+      let styL = sty(dragDom, 'left');
+      let styT = sty(dragDom, 'top');
+
+      // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
+      if (styL.includes('%')) {
+        styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100);
+        styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100);
+      } else {
+        styL = +styL.replace(/\px/g, '');
+        styT = +styT.replace(/\px/g, '');
+      }
+
+      document.onmousemove = function(e) {
+        // 通过事件委托,计算移动的距离
+        let left = e.clientX - disX;
+        let top = e.clientY - disY;
+
+        // 边界处理
+        if (-left > minDragDomLeft) {
+          left = -minDragDomLeft;
+        } else if (left > maxDragDomLeft) {
+          left = maxDragDomLeft;
+        }
+
+        if (-top > minDragDomTop) {
+          top = -minDragDomTop;
+        } else if (top > maxDragDomTop) {
+          top = maxDragDomTop;
+        }
+
+        // 移动当前元素
+        dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
+      };
+
+      document.onmouseup = function(e) {
+        document.onmousemove = null;
+        document.onmouseup = null;
+      };
+    };
+  },
+});

+ 30 - 0
src/components/common/i18n.js

@@ -0,0 +1,30 @@
+export const messages = {
+  zh: {
+    i18n: {
+      breadcrumb: '国际化产品',
+      tips: '通过切换语言按钮,来改变当前内容的语言。',
+      btn: '切换英文',
+      title1: '常用用法',
+      p1: '要是你把你的秘密告诉了风,那就别怪风把它带给树。',
+      p2: '没有什么比信念更能支撑我们度过艰难的时光了。',
+      p3: '只要能把自己的事做好,并让自己快乐,你就领先于大多数人了。',
+      title2: '组件插值',
+      info: 'Element组件需要国际化,请参考 {action}。',
+      value: '文档',
+    },
+  },
+  en: {
+    i18n: {
+      breadcrumb: 'International Products',
+      tips: 'Click on the button to change the current language. ',
+      btn: 'Switch Chinese',
+      title1: 'Common usage',
+      p1: 'If you reveal your secrets to the wind you should not blame the wind for  revealing them to the trees.',
+      p2: 'Nothing can help us endure dark times better than our faith. ',
+      p3: "If you can do what you do best and be happy, you're further along in life  than most people.",
+      title2: 'Component interpolation',
+      info: 'The default language of Element is Chinese. If you wish to use another language, please refer to the {action}.',
+      value: 'documentation',
+    },
+  },
+};

+ 50 - 0
src/components/frame/filter-page-table.md

@@ -0,0 +1,50 @@
+## filter-page-table.vue
+#### prop
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|fields|Array|`-`|是|字段列表(下文会说明如何使用)|
+|data|Array|`-`|是|数据列表|
+|opera|Array|[ ]|否|操作列的列表(下文会说明如何使用)|
+|toFormat|Function|`-`|否|如果fields中的format不是function类型,则会走toFormat的方法,需要自己写过滤规则,多个的情况需要区分|
+|select|Boolean|false|否|需要选择就变成true|
+|total|NUmber|0|否|分页的总数据|
+|usePage|Boolean|true|否|是否使用分页|
+|options|Object|null|否|加些属性,不知道能加啥,反正我把合计加上好使了|
+|useSum|Boolean|false|否|使用合计|
+|filter|Array|`[]`|否|额外查询|
+|operaWidth|Number|200|否|操作栏宽度|
+
+>fields
+>>
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|label|String|`-`|是|列名称|
+|prop|String|`-`|是|字段名称|
+|format|Function/String|`-`|否|Function类型:数据需要过滤则将过滤方法写在这;String类型:走toFormat方法,参数位(model=>字段名,value=>值)|
+|custom|Boolean|false|否|自定义输出|
+|options|Object|`-`|否|添加额外属性,比如说样式之类的|
+|filter|String|`-`|否|如果填写,则这个字段会查询,这里只填写类型,input/select,select的选项在options插槽中使用|
+|selected|Array|`-`|false|多选选项的数据|
+|showTip|Boolean|false|否|是否使用tooltip显示过长内容|
+|filterReturn|Boolean|`-`|否|针对这个选项需要在选择后就做些逻辑处理时,改成true,然后再使用filterReturn方法处理,(例如二级联动的情况)|
+|notable|Boolean|false/undefined|否|不需要在表格中显示|
+|selected|Array|`-`|false|多选选项的数据|
+
+>opera
+>>
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|label|String|`-`|是|操作按钮提示文字|
+|icon|String|`-`|否|图标|
+|method|String|`-`|是|此按钮连接的父级方法($emit)|
+|confirm|Boolean|`-`|否|是否需要确认提示|
+|methodZh|String/Function|label|否|确认提示的操作文字,1,Function参数为这条数据,自己随意组合;2,String为纯自定义字符串,需要自己写整个提示语;3,默认,使用label字段提示|
+|display|Function|`-`|否|控制按钮是否显示(目前为简单版,只是根据此条数据中的内容判断,以后要是有需求会修改成toFormat的形式)|
+
+>methods
+>>
+|方法名|参数|说明|
+|:-:|:-:|:-:|
+|handleSelect|Array[object]|返回选择的内容|
+|query|{skip,limit,...info}|分页查询,及条件查询|
+|filterReturn|{data,prop}|查询条件栏过滤条件中filterReturn字段为true的回调方法|

+ 299 - 0
src/components/frame/filter-page-table.vue

@@ -0,0 +1,299 @@
+<template>
+  <div id="data-table">
+    <el-form :model="searchInfo" :inline="true" style="padding:0.9rem 1.875rem ;" size="mini" v-if="useFilter">
+      <el-form-item v-for="(item, index) in filterList" :key="index">
+        <template v-if="item.filter === 'select'">
+          <el-select
+            v-model="searchInfo[item.prop]"
+            size="mini"
+            clearable
+            filterable
+            :placeholder="`请选择${item.label}`"
+            @clear="toClear(item.prop)"
+            @change="data => filterReturn(data, item)"
+          >
+            <slot name="options" v-bind="{ item }"></slot>
+          </el-select>
+        </template>
+        <template v-else-if="item.filter === 'date'">
+          <el-date-picker
+            v-model="searchInfo[item.prop]"
+            value-format="yyyy-MM-dd"
+            format="yyyy-MM-dd"
+            type="daterange"
+            range-separator="-"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            clearable
+          >
+          </el-date-picker>
+        </template>
+        <template v-else>
+          <el-input v-model="searchInfo[item.prop]" clearable size="mini" :placeholder="`请输入${item.label}`" @clear="toClear(item.prop)"></el-input>
+        </template>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" size="mini" @click="filterSearch">查询</el-button>
+      </el-form-item>
+    </el-form>
+    <el-table
+      ref="table"
+      row-key="id"
+      :data="data"
+      border
+      stripe
+      size="mini"
+      :max-height="height !== null ? height : ''"
+      @select="handleSelectionChange"
+      @select-all="handleSelectAll"
+      v-bind="options"
+      :show-summary="useSum"
+      @row-click="rowClick"
+    >
+      <el-table-column type="selection" width="55" v-if="select" prop="id" :reserve-selection="true"> </el-table-column>
+      <template v-for="(item, index) in fields">
+        <template v-if="!item.notable">
+          <template v-if="item.custom">
+            <el-table-column :key="index" align="center" :label="item.label" v-bind="item.options">
+              <template v-slot="{ row }">
+                <slot name="custom" v-bind="{ item, row }"></slot>
+              </template>
+            </el-table-column>
+          </template>
+          <template v-else>
+            <el-table-column
+              :key="index"
+              align="center"
+              :label="item.label"
+              :prop="item.prop"
+              :formatter="toFormatter"
+              sortable
+              v-bind="item.options"
+              :show-overflow-tooltip="item.showTip"
+            >
+            </el-table-column>
+          </template>
+        </template>
+      </template>
+      <template v-if="opera.length > 0">
+        <el-table-column label="操作" align="center" :width="operaWidth">
+          <template v-slot="{ row, $index }">
+            <template v-for="(item, index) in opera">
+              <template v-if="display(item, row)">
+                <el-tooltip v-if="item.icon" :key="index" effect="dark" :content="item.label" placement="bottom">
+                  <el-button
+                    :key="index"
+                    type="text"
+                    :icon="item.icon || ''"
+                    size="mini"
+                    @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)"
+                  ></el-button>
+                </el-tooltip>
+                <el-button v-else :key="index" type="text" size="mini" @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)">
+                  {{ item.label }}
+                </el-button>
+              </template>
+            </template>
+          </template>
+        </el-table-column>
+      </template>
+    </el-table>
+    <el-row type="flex" align="middle" justify="end" style="padding-top:1rem" v-if="usePage">
+      <el-col :span="24" style="text-align:right;">
+        <el-pagination
+          background
+          layout="total, prev, pager, next"
+          :page-sizes="[10, 15, 20, 50, 100]"
+          :total="total"
+          :page-size="limit"
+          :current-page.sync="currentPage"
+          @current-change="changePage"
+          @size-change="sizeChange"
+        >
+        </el-pagination>
+        <!-- sizes -->
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+export default {
+  name: 'data-table',
+  props: {
+    fields: { type: Array, required: true },
+    data: { type: Array, required: true },
+    opera: { type: Array, default: () => [] },
+    toFormat: null,
+    height: null,
+    select: { type: Boolean, default: false },
+    selected: { type: Array, default: () => [] },
+    usePage: { type: Boolean, default: true },
+    total: { type: Number, default: 0 },
+    options: null,
+    useSum: { type: Boolean, default: false },
+    filter: { type: Array, default: () => [] },
+    operaWidth: { type: Number, default: 200 },
+  },
+  components: {},
+  data: () => ({
+    pageSelected: [],
+    currentPage: 1,
+    limit: _.get(this, `$limit`, undefined) !== undefined ? this.$limit : process.env.VUE_APP_LIMIT * 1 || 10,
+    searchInfo: {},
+    useFilter: true,
+    filterList: [],
+  }),
+  created() {},
+  computed: {},
+  methods: {
+    toFormatter(row, column, cellValue, index) {
+      let this_fields = this.fields.filter(fil => fil.prop === column.property);
+      if (this_fields.length > 0) {
+        let format = _.get(this_fields[0], `format`, false);
+        if (format) {
+          let res;
+          if (_.isFunction(format)) {
+            res = format(cellValue);
+          } else {
+            res = this.toFormat({
+              model: this_fields[0].prop,
+              value: cellValue,
+            });
+          }
+          return res;
+        } else return cellValue;
+      }
+    },
+    handleOpera(data, method, confirm = false, methodZh, label, index) {
+      let self = true;
+      if (_.isFunction(methodZh)) {
+        methodZh = methodZh(data);
+      } else if (!_.isString(methodZh)) {
+        methodZh = label;
+        self = false;
+      }
+      if (confirm) {
+        this.$confirm(self ? methodZh : `您确认${methodZh}该数据?`, '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        })
+          .then(() => {
+            this.$emit(method, { data, index });
+          })
+          .catch(() => {});
+      } else {
+        this.$emit(method, { data, index });
+      }
+    },
+    handleSelectionChange(selection, row) {
+      // console.log(selection);
+      // console.log(row);
+      //根据row是否再pageSelected中,判断是添加还是删除
+      let res = [];
+      if (this.pageSelected.find(i => i.id === row.id)) {
+        res = this.pageSelected.filter(f => f.id !== row.id);
+      } else {
+        this.pageSelected.push(row);
+        res = this.pageSelected;
+      }
+      this.$set(this, `pageSelected`, res);
+      this.$emit(`handleSelect`, _.uniqBy(res, 'id'));
+    },
+    handleSelectAll(selection) {
+      //处于没全选状态,选择之后一定是全选,只有处于全选状态时,才会反选(全取消)
+      // console.log(selection);
+      let res = [];
+      if (selection.length > 0) {
+        //全选
+        res = _.uniqBy(this.pageSelected.concat(selection), 'id');
+      } else {
+        //全取消
+        res = _.differenceBy(this.pageSelected, this.data, 'id');
+      }
+      this.$set(this, `pageSelected`, res);
+      this.$emit(`handleSelect`, res);
+    },
+    initSelection() {
+      this.$nextTick(() => {
+        this.$refs.table.clearSelection();
+        this.selected.forEach(info => {
+          let d = this.data.filter(p => p.id === info.id);
+          if (d.length > 0) this.$refs.table.toggleRowSelection(d[0]);
+        });
+      });
+    },
+    selectReset() {
+      this.$refs.table.clearSelection();
+    },
+    display(item, row) {
+      let display = _.get(item, `display`, true);
+      if (display === true) return true;
+      else {
+        let res = display(row);
+        return res;
+      }
+    },
+    //
+    changePage(page = this.currentPage) {
+      this.$emit('query', { skip: (page - 1) * this.limit, limit: this.limit, ...this.searchInfo });
+    },
+    sizeChange(limit) {
+      this.limit = limit;
+      this.currentPage = 1;
+      this.$emit('query', { skip: 0, limit: this.limit, ...this.searchInfo });
+    },
+    getFilterList() {
+      let res = this.fields.filter(f => _.get(f, 'filter', false));
+      this.$set(this, `useFilter`, res.length > 0);
+      res.map(i => {
+        if (i.filter === 'date' && this.searchInfo[i.porp] === undefined) this.$set(this.searchInfo, i.prop, []);
+      });
+      res = [...res, ...this.filter];
+      this.$set(this, `filterList`, res);
+    },
+    filterSearch() {
+      this.currentPage = 1;
+      this.$emit('query', { skip: 0, limit: this.limit, ...this.searchInfo });
+    },
+    rowClick(row, column, event) {
+      this.$emit(`rowClick`, row);
+    },
+    toClear(prop) {
+      delete this.searchInfo[prop];
+    },
+    filterReturn(data, item) {
+      let { prop, filterReturn } = item;
+      if (filterReturn) this.$emit('filterReturn', { data, prop });
+    },
+  },
+  watch: {
+    selected: {
+      handler(val) {
+        if (val.length > 0) {
+          this.pageSelected = val;
+          this.initSelection();
+        }
+      },
+      immediate: true,
+    },
+    data: {
+      handler(val, oval) {
+        if (this.select) {
+          this.initSelection();
+        }
+      },
+    },
+    fields: {
+      handler(val, oval) {
+        if (val) this.getFilterList();
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 82 - 0
src/components/frame/form.md

@@ -0,0 +1,82 @@
+# 组件说明文档
+### form.vue
+### props
+
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|fields|Array|`-`|是|字段相关都在这里,用来自动输出,详情见下面|
+|submitText|String|`保存`|否|默认保存按钮的文字|
+|rules|Object|`-`|否|校验规则,不会找el-form的例子,不过使用的async-validator这个依赖为基础,会写这个也可以~~(那就厉害了,反正我是不行)~~|
+|isNew|Boolean|`-`|是|修改还是添加的提示|
+|data|Object|`-`|否|修改传来的数据|
+|needSave|Boolean|false|否|是否禁用保存按钮|
+|useEnter|Boolean|true|否|使用回车提交|
+|reset|Boolean|true|否|提交后是否重置表单|
+
+
+### fields
+>Array类型 必填
+>>
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|label|String|`-`|是|显示的字段中文|
+|type|String|input|否|这个字段要用什么类型来输出 input的基本类型可选值:date,datetime,radio,checkbox,select,text(只显示值),editor(富文本编辑器),password|
+|required|Boolean|`-`|否|是否必须输入|
+|model|String|`-`|是|字段名|
+|placeholder|String|`-`|否|占位,正常用,只是个透传|
+|options|object|`-`|否|标签的属性设置,例如:textarea 需要显示剩余字数,或者input限制长度,都往这里写,key-value形式(键值对,json的基本了解,不知道百度,具体属性看你具体用那个组件,那个组件有什么属性,瞎写不一定好使)|
+|custom|Boolean|`-`|否|是否使用自定义插槽|
+|tip|String|`-`|否|提示语,例如:请输入11位电话号码|
+|labelWidth|String|`120px`|否|表单label宽度,element的,默认120px|
+|format|Function|`-`|否|当type = text 时需要将该字段内容转换,可以使用format|
+
+
+
+
+
+
+
+### slot
+>
+|插槽名|说明|
+|:-:|:-:|
+|options|fields中type为select的,选项都写在这个插槽中,多个select则需要区分options所属问题|
+|radios|fields中type为radio的,选项都写在这个插槽中,多个radio则需要区分radios所属问题|
+|checkbox|fields中type为checkbox的,选项都写在这个插槽中,多个checkbox则需要区分checkboxs所属问题|
+|custom|自定义插槽,完全自己去写|
+|submit|提交按钮部分,当needSave为false时才可以使用|
+>>关于自定义的用法:
+>>在fields中,custom:true的情况即需要自定义,写法如下
+
+>>`<template #custom="{ item, form, fieldChange }"> ... </template>`
+>>
+|参数名|说明|
+|:-:|:-:|
+|item|fields循环出来的每一项|
+|form|组件内部的表单|
+|fieldChange|组件内部的修改方法,此方法不一定必须使用,看情况来;参数:{model:xxx,value:XXX}(model:字段名,value:值)|
+>>在使用时,此插槽内的v-model可以写成form[item.model],也可以写成form.字段名
+
+>>例如`<el-input v-model="form[item.model]">`或者`<el-input v-model="form.xxx">`
+
+>> **如果有多处需要自定义,请区分开去写**
+
+
+***
+### upload
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|url|String|`-`|是|上传地址|
+|limit|Number|`-`|是|限制上传数量|
+|data|any|`-`|否|上传数据|
+|type|String|`-`|否|上传返回的字段|
+|isBtn|Boolean|false|否|是否只显示按钮|
+|showList|Boolean|true|否|是否显示上传列表|
+|accept|String|`-`|否|可以上传的文件类型,不写就没限制|
+|tip|String|`-`|否|提示信息|
+|listType|String|picture-card|否|上传文件列表显示类型|
+
+>### method
+>|方法名|返回参数|说明|
+|:-:|:-:|:-:|
+|upload|{type,data}|上传成功返回

+ 211 - 0
src/components/frame/form.vue

@@ -0,0 +1,211 @@
+<template>
+  <div id="add">
+    <el-form
+      ref="form"
+      :model="form"
+      :rules="rules"
+      :label-width="labelWidth"
+      class="form"
+      size="small"
+      @submit.native.prevent
+      :style="styles"
+      :inline="inline"
+    >
+      <template v-for="(item, index) in fields">
+        <template v-if="!loading">
+          <el-form-item v-if="display(item)" :key="'form-field-' + index" :label="getField('label', item)" :prop="item.model" :required="item.required">
+            <template v-if="!item.custom">
+              <template v-if="item.type !== 'text'">
+                <el-tooltip class="item" effect="dark" :content="item.tip" placement="top-start" :disabled="!item.tip">
+                  <template v-if="item.type === `date` || item.type === `datetime`">
+                    <el-date-picker
+                      v-model="form[item.model]"
+                      :type="item.type"
+                      placeholder="选择择"
+                      format="yyyy-MM-dd"
+                      value-format="yyyy-MM-dd"
+                      v-bind="item.options"
+                    >
+                    </el-date-picker>
+                  </template>
+                  <template v-else-if="item.type === `year` || item.type === `week` || item.type === `day`">
+                    <el-date-picker
+                      v-model="form[item.model]"
+                      :type="item.type"
+                      placeholder="选择择"
+                      :format="`${item.type === 'year' ? 'yyyy' : item.type === 'week' ? 'MM' : 'dd'}`"
+                      :value-format="`${item.type === 'year' ? 'yyyy' : item.type === 'week' ? 'MM' : 'dd'}`"
+                      v-bind="item.options"
+                    >
+                    </el-date-picker>
+                  </template>
+                  <template v-else-if="item.type === 'time'">
+                    <el-time-picker v-model="form[item.model]" placeholder="请选择时间" format="HH:mm" value-format="HH:mm"></el-time-picker>
+                  </template>
+                  <template v-else-if="item.type === 'radio'">
+                    <el-radio-group v-model="form[item.model]" size="mini" v-bind="item.options">
+                      <slot name="radios" v-bind="{ item, form, fieldChange }"></slot>
+                    </el-radio-group>
+                  </template>
+                  <template v-else-if="item.type === 'checkbox'">
+                    <el-checkbox-group v-model="form[item.model]" v-bind="item.options">
+                      <slot name="checkboxs" v-bind="{ item, form, fieldChange }"></slot>
+                    </el-checkbox-group>
+                  </template>
+                  <template v-else-if="item.type === 'select'">
+                    <el-select v-model="form[item.model]" v-bind="item.options" filterable clearable>
+                      <slot name="options" v-bind="{ item, form, fieldChange }"></slot>
+                    </el-select>
+                  </template>
+                  <template v-else-if="item.type === 'textarea'">
+                    <el-input clearable v-model="form[item.model]" type="textarea" :autosize="{ minRows: 3, maxRows: 5 }"></el-input>
+                  </template>
+                  <template v-else-if="item.type === 'editor'">
+                    <wang-editor v-model="form[item.model]"></wang-editor>
+                  </template>
+                  <template v-else>
+                    <el-input
+                      clearable
+                      v-model="form[item.model]"
+                      :type="getField('type', item)"
+                      :placeholder="getField('placeholder', item)"
+                      :show-password="getField('type', item) === 'password'"
+                      v-bind="item.options"
+                    ></el-input>
+                  </template>
+                </el-tooltip>
+              </template>
+              <template v-else>
+                <template v-if="item.format">
+                  {{ item.format(form[item.model]) }}
+                </template>
+                <template v-else>
+                  {{ form[item.model] }}
+                </template>
+              </template>
+            </template>
+            <template v-else>
+              <slot name="custom" v-bind="{ item, form, fieldChange }"></slot>
+            </template>
+          </el-form-item>
+        </template>
+      </template>
+      <el-form-item label="" v-if="needSave" class="btn">
+        <el-row type="flex" align="middle" justify="start">
+          <el-col :span="6">
+            <el-button type="primary" @click="save">{{ submitText }}</el-button>
+          </el-col>
+        </el-row>
+      </el-form-item>
+      <el-form-item v-else>
+        <slot name="submit"></slot>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import wangEditor from '@/components/frame/wang-editor.vue';
+export default {
+  name: 'add',
+  props: {
+    fields: { type: Array, default: () => [] },
+    rules: { type: Object, default: () => {} },
+    isNew: { type: Boolean, default: true },
+    data: null,
+    styles: { type: Object, default: () => {} },
+    needSave: { type: Boolean, default: true },
+    labelWidth: { type: String, default: '120px' },
+    useEnter: { type: Boolean, default: true },
+    submitText: { type: String, default: '保存' },
+    inline: { type: Boolean, default: false },
+    reset: { type: Boolean, default: true },
+  },
+  components: {
+    wangEditor,
+  },
+  data: () => ({
+    form: {},
+    show: false,
+    dateShow: false,
+    loading: true,
+  }),
+  created() {
+    if (this.useEnter) {
+      document.onkeydown = () => {
+        let key = window.event.keyCode;
+        if (key == 13) {
+          this.save();
+        }
+      };
+    }
+  },
+  computed: {},
+  mounted() {},
+  watch: {
+    fields: {
+      handler(val) {
+        this.checkType();
+      },
+      immediate: true,
+    },
+    data: {
+      handler(val) {
+        this.loading = true;
+        if (val) this.$set(this, `form`, this.data);
+        this.loading = false;
+      },
+      immediate: true,
+      deep: true,
+    },
+  },
+  methods: {
+    getField(item, data) {
+      let res = _.get(data, item, null);
+      if (item === 'type') res = res === null ? `text` : res;
+      if (item === 'placeholder') res = res === null ? `请输入${data.label}` : res;
+      if (item === 'required') res = res === null ? false : res;
+      if (item === `error`) res = res === null ? `${data.label}错误` : res;
+      return res;
+    },
+    save() {
+      this.$refs['form'].validate(valid => {
+        if (valid) {
+          this.$emit(`save`, { isNew: this.isNew, data: JSON.parse(JSON.stringify(this.form)) });
+          if (this.reset) this.$refs.form.resetFields();
+        } else {
+          console.warn('form validate error!!!');
+        }
+      });
+    },
+    fieldChange({ model, value }) {
+      this.$set(this.form, model, value);
+    },
+    checkType() {
+      let arr = this.fields.filter(fil => fil.type === 'checkbox');
+      if (arr.length > 0 && this.isNew) {
+        for (const item of arr) {
+          this.$set(this.form, `${item.model}`, []);
+        }
+      }
+    },
+    display(field) {
+      let dis = _.get(field, `display`);
+      if (!_.isFunction(dis)) return true;
+      else return dis(field, this.form);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.form {
+  padding: 2rem 1rem;
+  background: #fff;
+  border-radius: 20px;
+}
+// /deep/.btn .el-form-item__content {
+//   margin-left: 0 !important;
+// }
+</style>

+ 11 - 0
src/components/frame/pagination.md

@@ -0,0 +1,11 @@
+# pagination.vue 分页组件
+#### props
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|position|String|'right'|否|组件的布局位置,默认为靠右侧|
+|total|Number|0|是|分页的总数据数,用来计算页码|
+|limit|Number|10|否|每页的数量|  
+#### methods
+|方法名|参数|说明|
+|:-:|:-:|:-:|
+|query|{skip,limit,...info}|分页查询|

+ 51 - 0
src/components/frame/pagination.vue

@@ -0,0 +1,51 @@
+<template>
+  <div id="pagination">
+    <el-row type="flex" align="middle" style="padding-top:1rem">
+      <el-col :span="24" :style="`text-align:${position};`">
+        <el-pagination
+          background
+          layout=" total, prev, pager, next"
+          :total="total"
+          :page-size="limit"
+          :current-page.sync="currentPage"
+          @current-change="changePage"
+        >
+        </el-pagination>
+        <!-- 
+          :page-sizes="[5, 10, 15, 20, 50, 100]"
+          @size-change="sizeChange"
+         -->
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+export default {
+  name: 'pagination',
+  props: {
+    position: { type: String, default: 'right' },
+    total: { type: Number, default: 0 },
+    limit: { type: Number, default: 10 },
+  },
+  components: {},
+  data: () => {
+    return {
+      currentPage: 1,
+    };
+  },
+  created() {},
+  methods: {
+    changePage(page) {
+      this.$emit('query', { skip: (page - 1) * this.limit, limit: this.limit });
+    },
+    sizeChange(limit) {
+      this.limit = limit;
+      this.$emit('query', { skip: 0, limit: this.limit });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 54 - 0
src/components/frame/qrcode.vue

@@ -0,0 +1,54 @@
+<template>
+  <div id="qrcode">
+    <img :src="dataUrl" />
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import QRCode from 'qrcode';
+export default {
+  name: 'qrcode',
+  props: {
+    qrcode: null,
+  },
+  components: {},
+  data: () => ({
+    dataUrl: null,
+    token: null,
+  }),
+  async mounted() {
+    await this.initQrcode();
+  },
+  created() {},
+  computed: {},
+  methods: {
+    async initQrcode() {
+      // 创建二维码
+      if (!this.qrcode) return;
+      let uri = `${Vue.config.weixin.baseUrl}/qrcode/${this.qrcode}/scan`;
+      if (uri.startsWith('/')) {
+        uri = `${location.protocol}//${location.host}${uri}`;
+      }
+      this.dataUrl = await QRCode.toDataURL(uri);
+      this.$stomp({
+        [`/exchange/qrcode.login/${this.qrcode}`]: this.onMessage,
+      });
+    },
+    onMessage(message) {
+      console.log('receive a message: ', message.body);
+      if (message.body == 'scaned') {
+        try {
+          this.$emit('toReturn', message);
+          console.log('扫码登录成功');
+        } catch (err) {
+          console.log('扫码登录失败');
+          console.error(err);
+        }
+      }
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 76 - 0
src/components/frame/uploadone.vue

@@ -0,0 +1,76 @@
+<template>
+  <div id="upload">
+    <el-upload
+      v-if="url"
+      ref="upload"
+      :action="url"
+      list-type="picture-card"
+      :file-list="fileList"
+      :limit="limit"
+      :on-exceed="outLimit"
+      :on-preview="handlePictureCardPreview"
+      :before-remove="handleRemove"
+      :on-success="onSuccess"
+      accept=".jpg,.jpeg,.png,.bmp,.gif,.svg"
+    >
+      <template>
+        <i class="el-icon-plus"></i>
+      </template>
+    </el-upload>
+    <el-dialog :visible.sync="dialogVisible">
+      <img width="100%" :src="dialogImageUrl" alt="" />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'upload',
+  props: {
+    url: { type: null },
+    limit: { type: Number },
+    data: { type: null },
+    type: { type: String },
+  },
+  components: {},
+  data: () => ({
+    dialogVisible: false,
+    dialogImageUrl: '',
+    fileList: [],
+  }),
+  created() {
+    if (this.data) {
+      this.defalutProcess(this.data);
+    }
+  },
+  watch: {
+    data: {
+      handler(val) {
+        this.defalutProcess(val);
+      },
+    },
+  },
+  computed: {},
+  methods: {
+    handlePictureCardPreview(file) {
+      this.dialogImageUrl = file.url;
+      this.dialogVisible = true;
+    },
+    handleRemove(file) {
+      return true;
+    },
+    outLimit() {
+      this.$message.error('只允许上传1张图片');
+    },
+    onSuccess(response, file, fileList) {
+      //将文件整理好传回父组件
+      this.$emit('upload', { type: this.type, data: response });
+    },
+    defalutProcess(val) {
+      this.$set(this, `fileList`, [{ name: this.type, url: `${this.data}?${new Date().getTime()}` }]);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 77 - 0
src/components/frame/wang-editor.vue

@@ -0,0 +1,77 @@
+<template>
+  <div ref="editor" style="text-align:left"></div>
+</template>
+<script>
+import E from 'wangeditor';
+
+const menus = [
+  'head', // 标题
+  'bold', // 粗体
+  'fontSize', // 字号
+  'fontName', // 字体
+  'italic', // 斜体
+  'underline', // 下划线
+  'strikeThrough', // 删除线
+  'foreColor', // 文字颜色
+  'backColor', // 背景颜色
+  'link', // 插入链接
+  'list', // 列表
+  'justify', // 对齐方式
+  'quote', // 引用
+  // 'emoticon', // 表情
+  'table', // 表格
+  // 'video', // 插入视频
+  // 'code', // 插入代码
+  'undo', // 撤销
+  'redo', // 重复
+];
+
+export default {
+  name: 'wang-editor',
+  model: {
+    prop: 'value',
+    event: 'change', // 默认为input时间,此处改为change
+  },
+  props: {
+    value: { type: String, required: false, default: '' },
+  },
+  data() {
+    return {
+      editorContent: this.value,
+    };
+  },
+  mounted() {
+    var editor = new E(this.$refs.editor);
+    editor.customConfig.onchange = html => {
+      this.editorContent = html;
+      this.$emit('change', html);
+    };
+    // 自定义菜单配置
+    editor.customConfig.menus = menus;
+    editor.customConfig.zIndex = 0;
+    editor.customConfig.uploadImgServer = '/files/cms/images/upload';
+    editor.customConfig.uploadImgMaxLength = 1;
+    editor.customConfig.uploadImgHooks = {
+      // 如果服务器端返回的不是 {errno:0, data: [...]} 这种格式,可使用该配置
+      // (但是,服务器端返回的必须是一个 JSON 格式字符串!!!否则会报错)
+      customInsert: function(insertImg, result, editor) {
+        // 图片上传并返回结果,自定义插入图片的事件(而不是编辑器自动插入图片!!!)
+        // insertImg 是插入图片的函数,editor 是编辑器对象,result 是服务器端返回的结果
+
+        // 举例:假如上传图片成功后,服务器端返回的是 {url:'....'} 这种格式,即可这样插入图片:
+        var url = result.uri;
+        insertImg(url);
+
+        // result 必须是一个 JSON 格式字符串!!!否则报错
+      },
+    };
+    editor.create();
+    editor.txt.html(this.value);
+  },
+  methods: {
+    getContent: function() {
+      return this.editorContent;
+    },
+  },
+};
+</script>

+ 55 - 0
src/components/page/403.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="error-page">
+    <div class="error-code">4<span>0</span>3</div>
+    <div class="error-desc">啊哦~ 你没有权限访问该页面哦</div>
+    <div class="error-handle">
+      <router-link to="/">
+        <el-button type="primary" size="large">返回首页</el-button>
+      </router-link>
+      <el-button class="error-btn" type="primary" size="large" @click="goBack">返回上一页</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  methods: {
+    goBack() {
+      this.$router.go(-1);
+    },
+  },
+};
+</script>
+
+<style scoped>
+.error-page {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+  background: #f3f3f3;
+  box-sizing: border-box;
+}
+.error-code {
+  line-height: 1;
+  font-size: 250px;
+  font-weight: bolder;
+  color: #f02d2d;
+}
+.error-code span {
+  color: #00a854;
+}
+.error-desc {
+  font-size: 30px;
+  color: #777;
+}
+.error-handle {
+  margin-top: 30px;
+  padding-bottom: 200px;
+}
+.error-btn {
+  margin-left: 100px;
+}
+</style>

+ 55 - 0
src/components/page/404.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="error-page">
+    <div class="error-code">4<span>0</span>4</div>
+    <div class="error-desc">啊哦~ 你所访问的页面不存在</div>
+    <div class="error-handle">
+      <router-link to="/">
+        <el-button type="primary" size="large">返回首页</el-button>
+      </router-link>
+      <el-button class="error-btn" type="primary" size="large" @click="goBack">返回上一页</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  methods: {
+    goBack() {
+      this.$router.go(-1);
+    },
+  },
+};
+</script>
+
+<style scoped>
+.error-page {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+  background: #f3f3f3;
+  box-sizing: border-box;
+}
+.error-code {
+  line-height: 1;
+  font-size: 250px;
+  font-weight: bolder;
+  color: #2d8cf0;
+}
+.error-code span {
+  color: #00a854;
+}
+.error-desc {
+  font-size: 30px;
+  color: #777;
+}
+.error-handle {
+  margin-top: 30px;
+  padding-bottom: 200px;
+}
+.error-btn {
+  margin-left: 100px;
+}
+</style>

+ 95 - 0
src/components/page/Dashboard.vue

@@ -0,0 +1,95 @@
+<template>
+  <div id="Dashboard">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 系统首页 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container">
+          <span v-if="user.js == '0'">
+            <zhstat :list="zhstatList" :sjstatList="sjstatList" :xsstatList="xsstatList" :khstatList="khstatList"></zhstat>
+          </span>
+          <span v-else-if="user.js == '1'">
+            <fhstat :sjstatList="sjstatList" :xsstatList="xsstatList" :khstatList="khstatList"></fhstat>
+          </span>
+          <span v-else>
+            <sjstat :xsstatList="xsstatList" :khstatList="khstatList"></sjstat>
+          </span>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import zhstat from './homeParts/zhstat.vue';
+import fhstat from './homeParts/fhstat.vue';
+import sjstat from './homeParts/sjstat.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: sj } = createNamespacedHelpers('sj');
+const { mapActions: xs } = createNamespacedHelpers('xs');
+const { mapActions: kh } = createNamespacedHelpers('kh');
+import bus from '../common/bus';
+export default {
+  metaInfo: { title: '系统首页' },
+  name: 'Dashboard',
+  props: {},
+  components: {
+    // 总行统计图
+    zhstat,
+    // 分行统计图
+    fhstat,
+    // 商家统计图
+    sjstat,
+  },
+  data: function() {
+    return {
+      // 总行
+      zhstatList: [],
+      // 商家
+      sjstatList: [],
+      // 销售
+      xsstatList: [],
+      // 客户
+      khstatList: [],
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...sj({ sJQuery: 'query' }),
+    ...xs({ xsQuery: 'query' }),
+    ...kh({ khQuery: 'query' }),
+    async search() {
+      // 支行
+      let res = await this.sJQuery({ js: 1 });
+      if (this.$checkRes(res)) {
+        this.$set(this, `zhstatList`, res.data);
+      }
+      // 商家
+      res = await this.sJQuery({ js: 2 });
+      if (this.$checkRes(res)) {
+        this.$set(this, `sjstatList`, res.data);
+      }
+      // 销售
+      res = await this.xsQuery();
+      if (this.$checkRes(res)) {
+        this.$set(this, `xsstatList`, res.data);
+      }
+      // 客户
+      res = await this.khQuery();
+      if (this.$checkRes(res)) {
+        this.$set(this, `khstatList`, res.data);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 112 - 0
src/components/page/Login.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="login-wrap">
+    <div class="ms-login">
+      <div class="ms-title">后台管理系统</div>
+      <el-form :model="form" :rules="rules" ref="login" label-width="0px" class="ms-content">
+        <el-form-item prop="username">
+          <el-input v-model="form.tel" placeholder="请输入手机号">
+            <el-button slot="prepend" icon="el-icon-user"></el-button>
+          </el-input>
+        </el-form-item>
+        <el-form-item prop="password">
+          <el-input type="password" placeholder="请输入密码" v-model="form.pwd">
+            <el-button slot="prepend" icon="el-icon-lock"></el-button>
+          </el-input>
+        </el-form-item>
+        <div class="login-btn">
+          <el-button type="primary" @click="submitForm()">登录</el-button>
+        </div>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: login } = createNamespacedHelpers('login');
+export default {
+  metaInfo: { title: '登录' },
+  name: 'login',
+  props: {},
+  components: {},
+  data: function() {
+    return {
+      form: {},
+      rules: {
+        tel: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+        pwd: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+      },
+    };
+  },
+  created() {},
+  methods: {
+    ...login(['login']),
+    submitForm() {
+      this.$refs.login.validate(async valid => {
+        if (valid) {
+          let res = await this.login({ user: this.form });
+          if (this.$checkRes(res)) {
+            this.$message.success('登录成功');
+            // localStorage.setItem('user', JSON.stringify(res.data));
+            this.$router.push('/dashboard');
+          }
+        } else {
+          this.$message.error('请输入账号和密码');
+          console.log('error submit!!');
+          return false;
+        }
+      });
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.login-wrap {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  background-image: url(../../assets/img/login-bg.jpg);
+  background-size: 100%;
+}
+.ms-title {
+  width: 100%;
+  line-height: 50px;
+  text-align: center;
+  font-size: 20px;
+  color: #fff;
+  border-bottom: 1px solid #ddd;
+}
+.ms-login {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  width: 350px;
+  margin: -190px 0 0 -175px;
+  border-radius: 5px;
+  background: rgba(255, 255, 255, 0.3);
+  overflow: hidden;
+}
+.ms-content {
+  padding: 30px 30px;
+}
+.login-btn {
+  text-align: center;
+}
+.login-btn button {
+  width: 100%;
+  height: 36px;
+  margin-bottom: 10px;
+}
+.login-tips {
+  font-size: 12px;
+  line-height: 30px;
+  color: #fff;
+}
+/deep/.js .el-form-item__content {
+  padding: 0 25px;
+  .el-radio {
+    color: #fff;
+  }
+}
+</style>

+ 209 - 0
src/components/page/branch/index.vue

@@ -0,0 +1,209 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 分行管理 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="12" class="left">
+              <el-input v-model="searchInput" placeholder="名字"></el-input>
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+            <el-col :span="12" class="right">
+              <el-button type="primary" size="small" @click="dialog = true">添加</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <data-table :fields="fields" :opera="opera" :data="list" :total="total" @view="toView" @edit="toEdit" @query="search"></data-table>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+    <el-dialog :visible.sync="dialog" title="分行用户信息" @close="toClose" :destroy-on-close="true" width="50%">
+      <data-form :data="form" :fields="formFields" :rules="rules" @save="turnSave"> </data-form>
+    </el-dialog>
+    <el-dialog :visible.sync="qrdialog" title="分行二维码" @close="toqrClose" width="20%">
+      <div style="text-align:center">
+        <img :src="qc" />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import dataForm from '@/components/frame/form.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: sj } = createNamespacedHelpers('sj');
+import QRCode from 'qrcode';
+export default {
+  metaInfo: { title: '分行用户管理' },
+  name: 'index',
+  props: {},
+  components: {
+    dataTable, //列表组件
+    dataForm, //表单组件
+  },
+  data: function() {
+    return {
+      // 搜索
+      searchInput: '',
+      // 分行用户功能操作+列表
+      opera: [
+        {
+          label: '查看二维码',
+          icon: 'el-icon-view',
+          method: 'view',
+        },
+        {
+          label: '修改',
+          icon: 'el-icon-edit',
+          method: 'edit',
+        },
+        // {
+        //   label: '删除',
+        //   icon: 'el-icon-delete',
+        //   method: 'delete',
+        // },
+      ],
+      fields: [
+        { label: '名字', prop: 'name' },
+        { label: '地址', prop: 'addr' },
+        { label: '电话', prop: 'tel' },
+      ],
+      list: [],
+      total: 0,
+      // 添加用户
+      dialog: false,
+      form: {},
+      formFields: [
+        { label: '名字', required: true, model: 'name' },
+        { label: '地址', required: false, model: 'addr' },
+        { label: '手机号', required: true, model: 'tel', options: { maxlength: 11, minlength: 11 } },
+        { label: '系统登录密码', required: false, model: 'pwd', type: 'password' },
+      ],
+      // 验证
+      rules: {
+        name: [{ required: true, message: '请输入名字', trigger: 'blur' }],
+        addr: [{ required: false, message: '请输入地址', trigger: 'blur' }],
+        tel: [{ required: true, message: '请输入电话', trigger: 'blur' }],
+        pwd: [{ required: false, message: '请输入密码', trigger: 'blur' }],
+      },
+      // 查看二维码
+      qrdialog: false,
+      qc: '',
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...sj(['query', 'fetch', 'create', 'update', 'delete']),
+    // 搜索数据
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      if (this.searchInput) {
+        info = {
+          name: this.searchInput,
+        };
+      }
+      const res = await this.query({ skip, limit, js: '1', ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+      }
+    },
+    // 查看二维码
+    // 分行二维码,扫码注册分行销售员,ssid:存储为分行id
+    async toView({ data }) {
+      this.qrdialog = true;
+      let url = `${process.env.NODE_ENV != 'development' ? 'http://jh.tgoodsoft.net' : Vue.config.weixin.baseUrl}/ccbmobile/saleRegister?ssid=${
+        data.id
+      }&ssjs=1`; // 需要转换为二维码的内容
+      let el = document.getElementById('qrCodeUrl');
+      this.qc = await QRCode.toDataURL(url, { height: 300, width: 300 });
+    },
+    // 修改
+    toEdit({ data }) {
+      this.$set(this, `form`, data);
+      this.dialog = true;
+    },
+    // 删除
+    // async toDelete({ data }) {
+    //   const res = await this.delete(data.tableid);
+    //   if (this.$checkRes(res)) {
+    //     this.$message({
+    //       message: '删除信息成功',
+    //       type: 'success',
+    //     });
+    //     this.search();
+    //   }
+    // },
+    // 保存用户
+    async turnSave({ data }) {
+      if (data.id) {
+        const res = await this.update({ ...data, id: data.tableid });
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '修改信息成功',
+            type: 'success',
+          });
+          this.toClose();
+          this.search();
+        }
+      } else {
+        data.ssjs = '0';
+        data.ssid = this.user.id;
+        data.js = '1';
+        data.srpt = '1';
+        const res = await this.create(data);
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '创建信息成功',
+            type: 'success',
+          });
+          this.toClose();
+          this.search();
+        }
+      }
+    },
+    // 取消添加
+    toClose() {
+      this.form = {};
+      this.dialog = false;
+    },
+    // 关闭二维码
+    toqrClose() {
+      this.qrdialog = false;
+    },
+    // 图片
+    uploadSuccess({ type, data }) {
+      this.$set(this.form, `${type}`, data.uri);
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downTop {
+    margin-bottom: 20px;
+    .left {
+      .el-input {
+        width: 89%;
+        margin: 0 10px 0 0;
+      }
+    }
+    .right {
+      text-align: right;
+    }
+  }
+}
+</style>

+ 245 - 0
src/components/page/business/index.vue

@@ -0,0 +1,245 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 商家用户管理 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="12" class="left">
+              <el-input v-model="searchInput" placeholder="名字"></el-input>
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+            <el-col :span="12" class="right">
+              <el-button type="primary" size="small" @click="dialog = true">添加</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <data-table
+              :fields="fields"
+              :opera="opera"
+              :data="list"
+              :total="total"
+              @view="toView"
+              @edit="toEdit"
+              @delete="toDelete"
+              @query="search"
+            ></data-table>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+    <el-dialog :visible.sync="dialog" title="商家用户信息" @close="toClose" :destroy-on-close="true" width="50%">
+      <data-form :data="form" :fields="formFields" :rules="rules" @save="turnSave">
+        <template #radios="{item}">
+          <template v-if="item.model == 'srpt'">
+            <el-radio :label="0">不可以</el-radio>
+            <el-radio :label="1">可以</el-radio>
+          </template>
+        </template>
+        <template #custom="{item}">
+          <template v-if="item.model == 'logo'">
+            <el-col :span="24">
+              <upload :limit="1" :data="form.logo" type="logo" :url="'/files/ccb/upload'" @upload="uploadSuccess"></upload>
+            </el-col>
+          </template>
+          <template v-if="item.model == 'advertpic'">
+            <el-col :span="24">
+              <upload :limit="1" :data="form.advertpic" type="advertpic" :url="'/files/ccb/upload'" @upload="uploadSuccess"></upload>
+            </el-col>
+          </template>
+        </template>
+      </data-form>
+    </el-dialog>
+    <el-dialog :visible.sync="qrdialog" title="商家二维码" @close="toqrClose" width="20%">
+      <div style="text-align:center">
+        <img :src="qc" />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import dataForm from '@/components/frame/form.vue';
+import upload from '@/components/frame/uploadone.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: sj } = createNamespacedHelpers('sj');
+import QRCode from 'qrcode';
+export default {
+  metaInfo: { title: '商家用户管理' },
+  name: 'index',
+  props: {},
+  components: {
+    dataTable, //列表组件
+    dataForm, //表单组件
+    upload, //图片组件
+  },
+  data: function() {
+    return {
+      // 搜索
+      searchInput: '',
+      // 商家用户功能操作+列表
+      opera: [
+        {
+          label: '查看二维码',
+          icon: 'el-icon-view',
+          method: 'view',
+        },
+        {
+          label: '修改',
+          icon: 'el-icon-edit',
+          method: 'edit',
+        },
+        {
+          label: '删除',
+          icon: 'el-icon-delete',
+          method: 'delete',
+        },
+      ],
+      fields: [
+        { label: '名字', prop: 'name' },
+        { label: '地址', prop: 'addr' },
+        { label: '电话', prop: 'tel' },
+        { label: '是否可登录系统', prop: 'srpt', format: i => (i == 0 ? '不可以' : '可以') },
+      ],
+      list: [],
+      total: 0,
+      // 添加用户
+      dialog: false,
+      form: {
+        srpt: 0,
+      },
+      formFields: [
+        { label: '名字', required: true, model: 'name' },
+        { label: '地址', required: false, model: 'addr' },
+        { label: '手机号', required: true, model: 'tel', placeholder: '系统登录用户名', options: { maxlength: 11, minlength: 11 } },
+        { label: '系统登录密码', required: true, model: 'pwd', type: 'password' },
+        { label: '是否可登录系统', required: true, model: 'srpt', type: 'radio' },
+        { label: '商家LOGO', required: false, model: 'logo', custom: true },
+        { label: '广告图', required: false, model: 'advertpic', custom: true },
+        { label: '商家说明', required: false, model: 'explain', type: 'textarea', options: { maxlength: 200 } },
+      ],
+      // 验证
+      rules: {
+        name: [{ required: true, message: '请输入名字', trigger: 'blur' }],
+        tel: [{ required: true, message: '请输入电话', trigger: 'blur' }],
+        pwd: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+      },
+      // 查看二维码
+      qrdialog: false,
+      qc: '',
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    // 搜索数据
+    ...sj(['query', 'fetch', 'create', 'update', 'delete']),
+    // 搜索数据
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      if (this.searchInput) {
+        info = {
+          name: this.searchInput,
+        };
+      }
+      const res = await this.query({ skip, limit, js: '2', ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+      }
+    },
+    // 查看二维码
+    // 商家二维码,扫码注册商家销售员,ssid:存储为商家id
+    async toView({ data }) {
+      this.qrdialog = true;
+      let url = `${process.env.NODE_ENV === 'development' ? 'http://jh.tgoodsoft.net' : Vue.config.weixin.baseUrl}/ccbmobile/saleRegister?ssid=${
+        data.tableid
+      }&ssjs=2`; // 需要转换为二维码的内容
+      let el = document.getElementById('qrCodeUrl');
+      this.qc = await QRCode.toDataURL(url, { height: 300, width: 300 });
+    },
+    // 修改
+    toEdit({ data }) {
+      this.$set(this, `form`, data);
+      this.dialog = true;
+    },
+    // 删除
+    async toDelete({ data }) {
+      const res = await this.delete(data.tableid);
+      if (this.$checkRes(res)) {
+        this.$message({
+          message: '删除信息成功',
+          type: 'success',
+        });
+        this.search();
+      }
+    },
+    // 保存用户
+    async turnSave({ data }) {
+      if (data.id) {
+        const res = await this.update({ ...data, id: data.tableid });
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '修改信息成功',
+            type: 'success',
+          });
+          this.toClose();
+          this.search();
+        }
+      } else {
+        data.ssjs = '1';
+        data.ssid = this.user.tableid;
+        data.js = '2';
+        const res = await this.create(data);
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '创建信息成功',
+            type: 'success',
+          });
+          this.toClose();
+          this.search();
+        }
+      }
+    },
+    // 取消添加
+    toClose() {
+      this.form = {};
+      this.dialog = false;
+    },
+    // 关闭二维码
+    toqrClose() {
+      this.qrdialog = false;
+    },
+    // 图片
+    uploadSuccess({ type, data }) {
+      this.$set(this.form, `${type}`, data.uri);
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downTop {
+    margin-bottom: 20px;
+    .left {
+      .el-input {
+        width: 89%;
+        margin: 0 10px 0 0;
+      }
+    }
+    .right {
+      text-align: right;
+    }
+  }
+}
+</style>

+ 141 - 0
src/components/page/fhSign/fhsjdeta.vue

@@ -0,0 +1,141 @@
+<template>
+  <div id="fhsjdeta">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 商家报名统计 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="4">
+              <el-select v-model="searchForm.sjid" clearable placeholder="请选择商家">
+                <el-option v-for="item in sjList" :key="item.id" :label="item.name" :value="item.tableid"> </el-option>
+              </el-select>
+            </el-col>
+            <el-col :span="6">
+              <el-date-picker
+                v-model="searchForm.date"
+                value-format="yyyy-MM-dd"
+                format="yyyy-MM-dd"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                clearable
+              >
+              </el-date-picker>
+            </el-col>
+            <el-col :span="14">
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <el-col :span="24" class="downListOne">
+              <el-col :span="12"> 客户总数{{ khtotal }}人 </el-col>
+              <el-col :span="12" style="text-align:right;">
+                <el-button type="primary" size="mini" @click="selectBtn()">导出数据</el-button>
+              </el-col>
+            </el-col>
+            <el-col :span="24" class="downListTwo">
+              <data-table :fields="fields" :opera="opera" :data="list" :total="total" @query="search"> </data-table>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: sj } = createNamespacedHelpers('sj');
+const { mapActions: xs } = createNamespacedHelpers('xs');
+const { mapActions: kh } = createNamespacedHelpers('kh');
+const { mapActions: count } = createNamespacedHelpers('count');
+export default {
+  metaInfo: { title: '商家报名统计' },
+  name: 'fhsjdeta',
+  props: {},
+  components: {
+    dataTable, //列表组件
+  },
+  data: function() {
+    return {
+      // 查询
+      searchForm: {},
+      sjList: [],
+      // 客户列表
+      opera: [],
+      fields: [
+        { label: '销售员姓名', prop: 'name' },
+        { label: '客户总数', prop: 'khtotal' },
+      ],
+      list: [],
+      total: 0,
+      // 客户总数
+      khtotal: 0,
+    };
+  },
+  created() {
+    this.searchSj();
+  },
+  methods: {
+    ...sj({ sjQuery: 'query' }),
+    ...xs({ xsQuery: 'query' }),
+    ...kh({ khQuery: 'query' }),
+    ...count({ countQuery: 'query', otherselectQuery: 'otherselectQuery' }),
+    async searchSj() {
+      let res = await this.sjQuery({ js: '2' });
+      if (this.$checkRes(res)) {
+        let data = res.data;
+        data.push({ name: '全部' });
+        this.$set(this, `sjList`, data);
+      }
+    },
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      info = {
+        ssid: this.searchForm.sjid,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+      };
+      let res = await this.countQuery({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+        let khtotal = res.data.reduce((p, n) => p + (n['khtotal'] * 1 || 0), 0);
+        if (khtotal) this.$set(this, `khtotal`, khtotal);
+      }
+    },
+    async selectBtn() {
+      let info = {
+        ssid: this.searchForm.sjid,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+      };
+      let res = await this.otherselectQuery({ ...info });
+      if (this.$checkRes(res)) {
+        window.open(res.data);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downList {
+    .downListOne {
+      padding: 15px 0;
+    }
+  }
+}
+</style>

+ 146 - 0
src/components/page/fhSign/fhxsdeta.vue

@@ -0,0 +1,146 @@
+<template>
+  <div id="fhxsdeta">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 销售员业绩明细 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="4">
+              <el-select v-model="searchForm.xsid" clearable placeholder="请选择销售员">
+                <el-option v-for="item in xsList" :key="item.id" :label="item.name" :value="item.id"> </el-option>
+              </el-select>
+            </el-col>
+            <el-col :span="6">
+              <el-date-picker
+                v-model="searchForm.date"
+                value-format="yyyy-MM-dd"
+                format="yyyy-MM-dd"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                clearable
+              >
+              </el-date-picker>
+            </el-col>
+            <el-col :span="4">
+              <el-input v-model="searchForm.tel" placeholder="请输入销售员手机号"></el-input>
+            </el-col>
+            <el-col :span="10" style="padding: 0 50px;">
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <el-col :span="24" class="downListOne">
+              <el-col :span="12"> 客户总数{{ khtotal }}人 </el-col>
+              <el-col :span="12" style="text-align:right;">
+                <el-button type="primary" size="mini" @click="selectBtn()">导出数据</el-button>
+              </el-col>
+            </el-col>
+            <el-col :span="24" class="downListTwo">
+              <data-table :fields="fields" :opera="opera" :data="list" :total="total" @query="search"></data-table>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: xs } = createNamespacedHelpers('xs');
+const { mapActions: kh } = createNamespacedHelpers('kh');
+const { mapActions: count } = createNamespacedHelpers('count');
+const { mapActions: khselect } = createNamespacedHelpers('khselect');
+export default {
+  metaInfo: { title: '销售员业绩明细' },
+  name: 'fhxsdeta',
+  props: {},
+  components: {
+    dataTable, //列表组件
+  },
+  data: function() {
+    return {
+      // 查询
+      searchForm: {},
+      xsList: [],
+      // 客户列表
+      opera: [],
+      fields: [
+        { label: '名字', prop: 'name' },
+        { label: '手机号', prop: 'tel' },
+      ],
+      list: [],
+      total: 0,
+      // 客户总数
+      khtotal: 0,
+    };
+  },
+  created() {
+    this.searchXs();
+  },
+  methods: {
+    ...xs({ xsQuery: 'query' }),
+    ...kh({ khQuery: 'query' }),
+    ...count({ countQuery: 'query' }),
+    ...khselect({ khselectQuery: 'khselectQuery' }),
+    async searchXs() {
+      let res = await this.xsQuery({ ssid: this.user.tableid });
+      if (this.$checkRes(res)) {
+        let data = res.data;
+        data.push({ name: '全部' });
+        this.$set(this, `xsList`, data);
+      }
+    },
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      info = {
+        id: this.searchForm.xsid,
+        tel: this.searchForm.tel,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+        needdata: true,
+      };
+      let res = await this.countQuery({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, _.get(res.data[0], 'list'));
+        this.$set(this, `total`, _.get(res.data[0], 'khtotal'));
+        this.$set(this, `khtotal`, _.get(res.data[0], 'khtotal'));
+      }
+    },
+    // 导出数据
+    async selectBtn() {
+      let info = {
+        xsid: this.searchForm.xsid,
+        tel: this.searchForm.tel,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+      };
+      let res = await this.khselectQuery({ ...info });
+      if (this.$checkRes(res)) {
+        window.open(res.data);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downList {
+    .downListOne {
+      padding: 15px 0;
+    }
+  }
+}
+</style>

+ 139 - 0
src/components/page/fhSign/fhxsstat.vue

@@ -0,0 +1,139 @@
+<template>
+  <div id="fhxsstat">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 销售员业绩统计 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="4">
+              <el-select v-model="searchForm.xsid" clearable placeholder="请选择销售员">
+                <el-option v-for="item in xsList" :key="item.id" :label="item.name" :value="item.id"> </el-option>
+              </el-select>
+            </el-col>
+            <el-col :span="6">
+              <el-date-picker
+                v-model="searchForm.date"
+                value-format="yyyy-MM-dd"
+                format="yyyy-MM-dd"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                clearable
+              >
+              </el-date-picker>
+            </el-col>
+            <el-col :span="14">
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <el-col :span="24" class="downListOne">
+              <el-col :span="12"> 客户总数{{ khtotal }}人 </el-col>
+              <el-col :span="12" style="text-align:right;">
+                <el-button type="primary" size="mini" @click="selectBtn()">导出数据</el-button>
+              </el-col>
+            </el-col>
+            <el-col :span="24" class="downListTwo">
+              <data-table :fields="fields" :opera="opera" :data="list" :total="total" @query="search"> </data-table>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: xs } = createNamespacedHelpers('xs');
+const { mapActions: kh } = createNamespacedHelpers('kh');
+const { mapActions: count } = createNamespacedHelpers('count');
+export default {
+  metaInfo: { title: '销售员业绩统计' },
+  name: 'fhxsstat',
+  props: {},
+  components: {
+    dataTable, //列表组件
+  },
+  data: function() {
+    return {
+      // 查询
+      searchForm: {},
+      xsList: [],
+      // 客户列表
+      opera: [],
+      fields: [
+        { label: '销售员姓名', prop: 'name' },
+        { label: '客户总数', prop: 'khtotal' },
+      ],
+      list: [],
+      total: 0,
+      // 客户总数
+      khtotal: 0,
+    };
+  },
+  created() {
+    this.searchXs();
+  },
+  methods: {
+    ...xs({ xsQuery: 'query' }),
+    ...kh({ khQuery: 'query' }),
+    ...count({ countQuery: 'query', otherselectQuery: 'otherselectQuery' }),
+    async searchXs() {
+      let res = await this.xsQuery({ ssid: this.user.tableid });
+      if (this.$checkRes(res)) {
+        let data = res.data;
+        data.push({ name: '全部' });
+        this.$set(this, `xsList`, data);
+      }
+    },
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      info = {
+        id: this.searchForm.xsid,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+      };
+      let res = await this.countQuery({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+        let khtotal = res.data.reduce((p, n) => p + (n['khtotal'] * 1 || 0), 0);
+        if (khtotal) this.$set(this, `khtotal`, khtotal);
+      }
+    },
+    async selectBtn() {
+      let info = {
+        id: this.searchForm.xsid,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+      };
+      let res = await this.otherselectQuery({ ...info });
+      if (this.$checkRes(res)) {
+        window.open(res.data);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downList {
+    .downListOne {
+      padding: 15px 0;
+    }
+  }
+}
+</style>

+ 170 - 0
src/components/page/homeParts/fhstat.vue

@@ -0,0 +1,170 @@
+<template>
+  <div id="fhstat">
+    <el-row>
+      <el-col :span="24" class="main">
+        <h3 class="h3">商家</h3>
+        <el-col :span="4" class="sjList" v-for="(item, sjindex) in sjList.slice(0, 6)" :key="`${sjindex}`">
+          <p>{{ item.name }}</p>
+          <p>
+            销售注册数:<span>{{ xsgetnum(item) || 0 }}</span
+            >&nbsp;人
+          </p>
+          <p>
+            客户报名数:<span>{{ khgetnum(item) || 0 }}</span
+            >&nbsp;人
+          </p>
+        </el-col>
+        <h3 class="h3 two">销售员</h3>
+        <el-col :span="4" class="xsList" v-for="(tag, xsindex) in xsList.slice(0, 6)" :key="xsindex + 'only'">
+          <p>{{ tag.name }}</p>
+          <p>
+            客户报名数:<span>{{ xskhgetnum(tag) || 0 }}&nbsp;人</span>
+          </p>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  metaInfo: { title: 'fhstat' },
+  name: 'fhstat',
+  props: {
+    // 商家
+    sjstatList: { type: Array },
+    // 销售
+    xsstatList: { type: Array },
+    // 客户
+    khstatList: { type: Array },
+  },
+  components: {},
+  data: function() {
+    return {
+      sjList: [],
+      xsList: [],
+    };
+  },
+  created() {},
+  methods: {
+    searchfilter() {
+      let sjList = this.sjstatList.filter(i => i.ssid == this.user.tableid);
+      if (sjList) this.$set(this, `sjList`, sjList);
+      let xsList = this.xsstatList.filter(i => i.ssid == this.user.tableid);
+      if (xsList) this.$set(this, `xsList`, xsList);
+    },
+    // 获取商家销售数
+    xsgetnum(data) {
+      let res = this.xsstatList.filter(i => i.ssid == data.tableid);
+      if (res) {
+        return res.length;
+      }
+    },
+    // 商家客户总数
+    khgetnum(data) {
+      let res = this.khstatList.filter(i => i.sjid == data.tableid);
+      if (res) {
+        return res.length;
+      }
+    },
+    // 销售员客户总数
+    xskhgetnum(data) {
+      let res = this.khstatList.filter(i => i.xsid == data.tableid);
+      if (res) {
+        return res.length;
+      }
+    },
+  },
+  watch: {
+    sjstatList: {
+      handler(val) {
+        this.searchfilter();
+      },
+    },
+    xsstatList: {
+      handler(val) {
+        this.searchfilter();
+      },
+    },
+    khstatList: {
+      handler(val) {
+        this.searchfilter();
+      },
+    },
+    immediate: true,
+    deep: true,
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.main {
+  .h3 {
+    float: left;
+    width: 100%;
+    margin: 15px 0;
+  }
+  .two {
+    margin: 70px 0 15px 0;
+  }
+  .sjList {
+    text-align: center;
+    border-radius: 360px;
+    margin: 5px 10px;
+    width: 15%;
+    height: 240px;
+    box-shadow: 0 0 6px #ccc;
+    p:nth-child(1) {
+      font-size: 20px;
+      font-family: 微软雅黑;
+      font-weight: bold;
+      padding: 50px 0 20px 0;
+    }
+    p:nth-child(2) {
+      font-size: 16px;
+      font-family: 微软雅黑;
+      font-weight: bold;
+      padding: 10px 0;
+      span {
+        color: red;
+      }
+    }
+    p:nth-child(3) {
+      font-size: 16px;
+      font-family: 微软雅黑;
+      font-weight: bold;
+      padding: 10px 0;
+      span {
+        color: red;
+      }
+    }
+  }
+  .xsList {
+    text-align: center;
+    border-radius: 360px;
+    margin: 5px 0 90px 10px;
+    width: 15%;
+    height: 240px;
+    box-shadow: 0 0 6px #ccc;
+    p:nth-child(1) {
+      font-size: 20px;
+      font-family: 微软雅黑;
+      font-weight: bold;
+      padding: 70px 0 20px 0;
+    }
+    p:nth-child(2) {
+      font-size: 16px;
+      font-family: 微软雅黑;
+      font-weight: bold;
+      padding: 15px 0;
+      span {
+        color: red;
+      }
+    }
+  }
+}
+</style>

+ 119 - 0
src/components/page/homeParts/sjstat.vue

@@ -0,0 +1,119 @@
+<template>
+  <div id="sjstat">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="5" class="list" v-for="(item, index) in xsList.slice(0, 8)" :key="index">
+          <p>{{ item.name }}</p>
+
+          <p>
+            客户报名数:<span>{{ khgetnum(item) || 0 }}</span
+            >&nbsp;人
+          </p>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  metaInfo: { title: 'sjstat' },
+  name: 'sjstat',
+  props: {
+    // 销售
+    xsstatList: { type: Array },
+    // 客户
+    khstatList: { type: Array },
+  },
+  components: {},
+  data: function() {
+    return {
+      xsList: [],
+    };
+  },
+  created() {},
+  methods: {
+    searchfilter() {
+      let xsList = this.xsstatList.filter(i => i.ssid == this.user.tableid);
+      if (xsList) this.$set(this, `xsList`, xsList);
+    },
+    khgetnum(data) {
+      let res = this.khstatList.filter(i => i.sjid == data.tableid);
+      if (res) {
+        return res.length;
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+  watch: {
+    xsstatList: {
+      handler(val) {
+        this.searchfilter();
+      },
+    },
+    khstatList: {
+      handler(val) {
+        this.searchfilter();
+      },
+    },
+    immediate: true,
+    deep: true,
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.list {
+  text-align: center;
+  border-radius: 360px;
+  margin: 28px 30px;
+  height: 320px;
+  -webkit-box-shadow: 0 0 6px #ccc;
+  box-shadow: 0 0 6px #ccc;
+  p:nth-child(1) {
+    font-size: 30px;
+    font-family: 微软雅黑;
+    font-weight: bold;
+    padding: 80px 0 40px 0;
+  }
+  p:nth-child(2) {
+    font-size: 18px;
+    font-family: 微软雅黑;
+    font-weight: bold;
+    padding: 10px 0;
+    span {
+      color: red;
+    }
+  }
+  p:nth-child(3) {
+    font-size: 18px;
+    font-family: 微软雅黑;
+    font-weight: bold;
+    padding: 10px 0;
+    span {
+      color: red;
+    }
+  }
+  p:nth-child(4) {
+    font-size: 18px;
+    font-family: 微软雅黑;
+    font-weight: bold;
+    padding: 10px 0;
+    span {
+      color: red;
+    }
+  }
+}
+.list:hover {
+  -webkit-transform: translateY(-6px);
+  -ms-transform: translateY(-6px);
+  transform: translateY(-6px);
+  -webkit-box-shadow: 0 0 6px #242f42;
+  box-shadow: 0 0 6px #242f42;
+  -webkit-transition: all 0.5s ease-out;
+  transition: all 0.5s ease-out;
+}
+</style>

+ 121 - 0
src/components/page/homeParts/zhstat.vue

@@ -0,0 +1,121 @@
+<template>
+  <div id="zhstat">
+    <el-row>
+      <el-col :span="24" class="zhstat">
+        <el-col :span="5" class="list" v-for="(item, index) in list.slice(0, 8)" :key="index">
+          <p>{{ item.name }}</p>
+          <p>
+            商家注册数:<span>{{ getsjnum(item) || 0 }}</span
+            >&nbsp;人
+          </p>
+          <p>
+            销售注册数:<span>{{ getxsnum(item) || 0 }}</span
+            >&nbsp;人
+          </p>
+          <p>
+            客户报名数:<span>{{ getkhnum(item) || 0 }}</span
+            >&nbsp;人
+          </p>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  metaInfo: { title: 'zhstat' },
+  name: 'zhstat',
+  props: {
+    list: { type: Array },
+    // 商家
+    sjstatList: { type: Array },
+    // 销售
+    xsstatList: { type: Array },
+    // 客户
+    khstatList: { type: Array },
+  },
+  components: {},
+  data: function() {
+    return {};
+  },
+  created() {},
+  methods: {
+    getsjnum(data) {
+      let res = this.sjstatList.filter(i => i.ssid == data.tableid);
+      if (res) {
+        return res.length;
+      }
+    },
+    getxsnum(data) {
+      let res = this.xsstatList.filter(i => i.ssid == data.tableid);
+      if (res) {
+        return res.length;
+      }
+    },
+    getkhnum(data) {
+      let res = this.khstatList.filter(i => i.fhid == data.tableid);
+      if (res) {
+        return res.length;
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.list {
+  text-align: center;
+  border-radius: 360px;
+  margin: 28px 30px;
+  height: 320px;
+  -webkit-box-shadow: 0 0 6px #ccc;
+  box-shadow: 0 0 6px #ccc;
+  p:nth-child(1) {
+    font-size: 30px;
+    font-family: 微软雅黑;
+    font-weight: bold;
+    padding: 60px 0 30px 0;
+  }
+  p:nth-child(2) {
+    font-size: 18px;
+    font-family: 微软雅黑;
+    font-weight: bold;
+    padding: 10px 0;
+    span {
+      color: red;
+    }
+  }
+  p:nth-child(3) {
+    font-size: 18px;
+    font-family: 微软雅黑;
+    font-weight: bold;
+    padding: 10px 0;
+    span {
+      color: red;
+    }
+  }
+  p:nth-child(4) {
+    font-size: 18px;
+    font-family: 微软雅黑;
+    font-weight: bold;
+    padding: 10px 0;
+    span {
+      color: red;
+    }
+  }
+}
+.list:hover {
+  -webkit-transform: translateY(-6px);
+  -ms-transform: translateY(-6px);
+  transform: translateY(-6px);
+  -webkit-box-shadow: 0 0 6px #242f42;
+  box-shadow: 0 0 6px #242f42;
+  -webkit-transition: all 0.5s ease-out;
+  transition: all 0.5s ease-out;
+}
+</style>

+ 210 - 0
src/components/page/peer/index.vue

@@ -0,0 +1,210 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 同级账号管理 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="12" class="left">
+              <el-input v-model="searchInput" placeholder="名字"></el-input>
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+            <el-col :span="12" class="right">
+              <el-button type="primary" size="small" @click="dialog = true">添加</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <data-table
+              :fields="fields"
+              :opera="opera"
+              :data="list"
+              :total="total"
+              @view="toView"
+              @edit="toEdit"
+              @delete="toDelete"
+              @query="search"
+            ></data-table>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+    <el-dialog :visible.sync="dialog" title="用户信息" @close="toClose" :destroy-on-close="true" width="50%">
+      <data-form :data="form" :fields="formFields" :rules="rules" @save="turnSave"> </data-form>
+    </el-dialog>
+    <el-dialog :visible.sync="qrdialog" title="账号二维码" @close="toqrClose" width="20%">
+      <div style="text-align:center">
+        <img :src="qc" />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import dataForm from '@/components/frame/form.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: user } = createNamespacedHelpers('user');
+import QRCode from 'qrcode';
+export default {
+  metaInfo: { title: '同级账号管理' },
+  name: 'index',
+  props: {},
+  components: {
+    dataTable, //列表组件
+    dataForm, //表单组件
+  },
+  data: function() {
+    return {
+      // 搜索
+      searchInput: '',
+      // 分行用户功能操作+列表
+      opera: [
+        {
+          label: '查看二维码',
+          icon: 'el-icon-view',
+          method: 'view',
+        },
+        {
+          label: '修改',
+          icon: 'el-icon-edit',
+          method: 'edit',
+        },
+        {
+          label: '删除',
+          icon: 'el-icon-delete',
+          method: 'delete',
+        },
+      ],
+      fields: [
+        { label: '名字', prop: 'name' },
+        { label: '电话', prop: 'tel' },
+      ],
+      list: [],
+      total: 0,
+      // 添加用户
+      dialog: false,
+      form: {},
+      formFields: [
+        { label: '名字', required: true, model: 'name' },
+        { label: '手机号', required: true, model: 'tel', options: { maxlength: 11, minlength: 11 } },
+        { label: '系统登录密码', required: false, model: 'pwd', type: 'password' },
+      ],
+      // 验证
+      rules: {
+        name: [{ required: true, message: '请输入名字', trigger: 'blur' }],
+        tel: [{ required: true, message: '请输入电话', trigger: 'blur' }],
+        pwd: [{ required: false, message: '请输入密码', trigger: 'blur' }],
+      },
+      // 查看二维码
+      qrdialog: false,
+      qc: '',
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...user(['query', 'fetch', 'create', 'update', 'delete']),
+    // 搜索数据
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      if (this.searchInput) {
+        info = {
+          name: this.searchInput,
+        };
+      }
+      const res = await this.query({ skip, limit, js: '0', ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+      }
+    },
+    // 查看二维码s
+    async toView({ data }) {
+      this.qrdialog = true;
+      let url = `${process.env.NODE_ENV === 'development' ? 'http://jh.tgoodsoft.net' : Vue.config.weixin.baseUrl}/ccbmobile/`; // 需要转换为二维码的内容
+      let el = document.getElementById('qrCodeUrl');
+      this.qc = await QRCode.toDataURL(url, { height: 300, width: 300 });
+    },
+    // 修改
+    toEdit({ data }) {
+      this.$set(this, `form`, data);
+      this.dialog = true;
+    },
+    // 删除
+    async toDelete({ data }) {
+      const res = await this.delete(data.id);
+      if (this.$checkRes(res)) {
+        this.$message({
+          message: '删除信息成功',
+          type: 'success',
+        });
+        this.search();
+      }
+    },
+    // 保存用户
+    async turnSave({ data }) {
+      if (data.id) {
+        const res = await this.update(data);
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '修改信息成功',
+            type: 'success',
+          });
+          this.toClose();
+          this.search();
+        }
+      } else {
+        data.js = '0';
+        data.srpt = '1';
+        const res = await this.create(data);
+        if (this.$checkRes(res)) {
+          this.$message({
+            message: '创建信息成功',
+            type: 'success',
+          });
+          this.toClose();
+          this.search();
+        }
+      }
+    },
+    // 取消添加
+    toClose() {
+      this.form = {};
+      this.dialog = false;
+    },
+    // 关闭二维码
+    toqrClose() {
+      this.qrdialog = false;
+    },
+    // 图片
+    uploadSuccess({ type, data }) {
+      this.$set(this.form, `${type}`, data.uri);
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downTop {
+    margin-bottom: 20px;
+    .left {
+      .el-input {
+        width: 89%;
+        margin: 0 10px 0 0;
+      }
+    }
+    .right {
+      text-align: right;
+    }
+  }
+}
+</style>

+ 136 - 0
src/components/page/sale/index.vue

@@ -0,0 +1,136 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 销售员管理 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="12" class="left">
+              <el-input v-model="searchInput" placeholder="名字"></el-input>
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <data-table :fields="fields" :opera="opera" :data="list" :total="total" @view="toView" @delete="toDelete" @query="search"></data-table>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+    <el-dialog :visible.sync="dialog" title="销售员二维码" @close="toClose" width="20%">
+      <div style="text-align:center">
+        <img :src="qc" />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: xs } = createNamespacedHelpers('xs');
+import QRCode from 'qrcode';
+export default {
+  metaInfo: { title: '销售员管理' },
+  name: 'index',
+  props: {},
+  components: {
+    dataTable, //列表组件
+  },
+  data: function() {
+    return {
+      // 搜索
+      searchInput: '',
+      // 分行用户功能操作+列表
+      opera: [
+        {
+          label: '二维码',
+          icon: 'el-icon-view',
+          method: 'view',
+        },
+        {
+          label: '删除',
+          icon: 'el-icon-delete',
+          method: 'delete',
+        },
+      ],
+      fields: [
+        { label: '名字', prop: 'name' },
+        { label: '手机号', prop: 'tel' },
+      ],
+      list: [],
+      total: 0,
+      // 二维码生成
+      vercode: '',
+      dialog: false,
+      qc: '',
+    };
+  },
+  created() {
+    this.search();
+  },
+
+  methods: {
+    // 搜索数据
+    ...xs(['query', 'fetch', 'create', 'update', 'delete']),
+    // 搜索数据
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      if (this.searchInput) {
+        info = {
+          name: this.searchInput,
+        };
+      }
+      const res = await this.query({ skip, limit, ssid: this.user.tableid, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+      }
+    },
+    // 生成二维码
+    async toView({ data }) {
+      this.dialog = true;
+      let url = `${process.env.NODE_ENV === 'development' ? 'http://jh.tgoodsoft.net' : Vue.config.weixin.baseUrl}/ccbmobile/user?xsid=${data.id}&js=${
+        this.user.js
+      }&ssid=${data.ssid}`; // 需要转换为二维码的内容
+      let el = document.getElementById('qrCodeUrl');
+      this.qc = await QRCode.toDataURL(url, { height: 300, width: 300 });
+    },
+    // 关闭二维码
+    toClose() {
+      this.dialog = false;
+    },
+    // 删除
+    async toDelete({ data }) {
+      const res = await this.delete(data.id);
+      if (this.$checkRes(res)) {
+        this.$message({
+          message: '删除信息成功',
+          type: 'success',
+        });
+        this.search();
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downTop {
+    margin-bottom: 20px;
+    .left {
+      .el-input {
+        width: 89%;
+        margin: 0 10px 0 0;
+      }
+    }
+  }
+}
+</style>

+ 84 - 0
src/components/page/setting/hdimage.vue

@@ -0,0 +1,84 @@
+<template>
+  <div id="hdimage">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 活动设置图管理 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container">
+          <data-form :data="form" :fields="fields" :rules="rules" @save="turnSave">
+            <template #custom="{item}">
+              <template v-if="item.model == 'hdimage'">
+                <el-col :span="24">
+                  <upload :limit="1" :data="form.hdimage" type="hdimage" :url="'/files/ccb/upload'" @upload="uploadSuccess" v-if="form.hdimage"></upload>
+                  <upload :limit="1" :data="nullImage" type="hdimage" :url="'/files/ccb/upload'" @upload="uploadSuccess" v-else></upload>
+                </el-col>
+              </template>
+              <template v-if="item.model == 'advertpic'">
+                <el-col :span="24">
+                  <upload :limit="1" :data="form.advertpic" type="advertpic" :url="'/files/ccb/upload'" @upload="uploadSuccess" v-if="form.advertpic"></upload>
+                  <upload :limit="1" :data="nullImage" type="advertpic" :url="'/files/ccb/upload'" @upload="uploadSuccess" v-else></upload>
+                </el-col>
+              </template>
+            </template>
+          </data-form>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import dataForm from '@/components/frame/form.vue';
+import upload from '@/components/frame/uploadone.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: xt } = createNamespacedHelpers('xt');
+export default {
+  metaInfo: { title: '活动设置图管理' },
+  name: 'hdimage',
+  props: {},
+  components: {
+    dataForm, //表单组件
+    upload, //上传图片
+  },
+  data: function() {
+    return {
+      form: {},
+      fields: [
+        { label: '宣传图设置', required: false, model: 'hdimage', custom: true },
+        { label: '广告图设置', required: false, model: 'advertpic', custom: true },
+      ],
+      // 验证
+      rules: {},
+      nullImage: '',
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...xt(['query', 'update']),
+    async search() {
+      let res = await this.query();
+      if (this.$checkRes(res)) {
+        this.$set(this, `form`, res.data);
+      }
+    },
+    // 活动图设置保存
+    async turnSave({ data }) {
+      let res = await this.update(data);
+    },
+    // 图片
+    uploadSuccess({ type, data }) {
+      this.$set(this.form, `${type}`, data.uri);
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 67 - 0
src/components/page/setting/personal.vue

@@ -0,0 +1,67 @@
+<template>
+  <div id="personal">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 个人信息维护管理 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container">
+          <data-form :data="form" :fields="fields" :rules="rules" @save="turnSave"> </data-form>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import dataForm from '@/components/frame/form.vue';
+import { mapState, mapMutations, createNamespacedHelpers } from 'vuex';
+const { mapActions: user } = createNamespacedHelpers('user');
+export default {
+  metaInfo: { title: '个人信息维护管理' },
+  name: 'personal',
+  props: {},
+  components: { dataForm },
+  data: function() {
+    return {
+      form: {},
+      fields: [
+        { label: '名字', required: true, model: 'name', options: {} },
+        { label: '手机号', required: true, model: 'tel', options: { maxlength: 11, minlength: 11, placeholder: '请谨慎修改手机,此处关联系统登录' } },
+      ],
+      // 验证
+      rules: {
+        name: [{ required: true, message: '请输入名字', trigger: 'blur' }],
+        tel: [{ required: true, message: '请输入电话,请谨慎修改手机,此处关联系统登录', trigger: 'blur' }],
+      },
+    };
+  },
+  created() {
+    if (this.user) {
+      this.$set(this, `form`, this.user);
+    }
+  },
+  methods: {
+    ...user(['update']),
+    ...mapMutations(['setUser']),
+    // 修改保存个人信息
+    async turnSave({ data }) {
+      let res = await this.update(data);
+      this.$message({
+        message: '修改信息成功',
+        type: 'success',
+      });
+      localStorage.setItem('user', JSON.stringify(res.data));
+      this.setUser();
+      location.reload();
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 66 - 0
src/components/page/setting/qrcode.vue

@@ -0,0 +1,66 @@
+<template>
+  <div id="qrcode">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 二维码查看 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container">
+          <img :src="qc" />
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+import QRCode from 'qrcode';
+export default {
+  metaInfo: { title: '二维码查看' },
+  name: 'qrcode',
+  props: {},
+  components: {},
+  data: function() {
+    return {
+      qc: '',
+    };
+  },
+  created() {
+    if (this.user) {
+      this.createQrcode();
+    }
+  },
+  methods: {
+    async createQrcode() {
+      let user = this.user;
+      console.log(this.user);
+      if (user.js == '0') {
+        let url = `${process.env.NODE_ENV === 'development' ? 'http://192.168.31.194:8002' : Vue.config.weixin.baseUrl}/ccbmobile/`; // 需要转换为二维码的内容
+        let el = document.getElementById('qrCodeUrl');
+        this.qc = await QRCode.toDataURL(url, { height: 300, width: 300 });
+      } else if (user.js == '1') {
+        let url = `${process.env.NODE_ENV === 'development' ? 'http://192.168.31.194:8002' : Vue.config.weixin.baseUrl}/ccbmobile/saleRegister?ssid=${
+          user.tableid
+        }&ssjs=1`; // 需要转换为二维码的内容
+        let el = document.getElementById('qrCodeUrl');
+        this.qc = await QRCode.toDataURL(url, { height: 300, width: 300 });
+      } else if (user.js == '2') {
+        let url = `${process.env.NODE_ENV === 'development' ? 'http://192.168.31.194:8002' : Vue.config.weixin.baseUrl}/ccbmobile/saleRegister?ssid=${
+          user.tableid
+        }&ssjs=2`; // 需要转换为二维码的内容
+        let el = document.getElementById('qrCodeUrl');
+        this.qc = await QRCode.toDataURL(url, { height: 300, width: 300 });
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 63 - 0
src/components/page/setting/updatepwd.vue

@@ -0,0 +1,63 @@
+<template>
+  <div id="updatepwd">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 修改密码 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container">
+          <data-form :data="form" :fields="fields" :rules="rules" @save="turnSave"> </data-form>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import dataForm from '@/components/frame/form.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: password } = createNamespacedHelpers('password');
+export default {
+  metaInfo: { title: '修改密码' },
+  name: 'updatepwd',
+  props: {},
+  components: {
+    dataForm,
+  },
+  data: function() {
+    return {
+      form: {},
+      fields: [{ label: '新密码', required: true, model: 'pwd', type: 'password' }],
+      // 验证
+      rules: {
+        pwd: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
+      },
+    };
+  },
+  created() {},
+  methods: {
+    ...password(['update']),
+    // 修改保存修改密码
+    async turnSave({ data }) {
+      data.id = this.user.id;
+      console.log(data);
+      let res = await this.update(data);
+      if (this.$checkRes(res)) {
+        this.$message({
+          message: '修改密码成功',
+          type: 'success',
+        });
+        localStorage.removeItem('user');
+        this.$router.push('/login');
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 146 - 0
src/components/page/sjSign/sjxsdeta.vue

@@ -0,0 +1,146 @@
+<template>
+  <div id="sjxsdeta">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 销售员业绩明细 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="4">
+              <el-select v-model="searchForm.xsid" clearable placeholder="请选择销售员">
+                <el-option v-for="item in xsList" :key="item.id" :label="item.name" :value="item.id"> </el-option>
+              </el-select>
+            </el-col>
+            <el-col :span="6">
+              <el-date-picker
+                v-model="searchForm.date"
+                value-format="yyyy-MM-dd"
+                format="yyyy-MM-dd"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                clearable
+              >
+              </el-date-picker>
+            </el-col>
+            <el-col :span="4">
+              <el-input v-model="searchForm.tel" placeholder="请输入手机号"></el-input>
+            </el-col>
+            <el-col :span="10" style="padding: 0 50px;">
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <el-col :span="24" class="downListOne">
+              <el-col :span="12"> 客户总数{{ khtotal }}人 </el-col>
+              <el-col :span="12" style="text-align:right;">
+                <el-button type="primary" size="mini" @click="selectBtn()">导出数据</el-button>
+              </el-col>
+            </el-col>
+            <el-col :span="24" class="downListTwo">
+              <data-table :fields="fields" :opera="opera" :data="list" :total="total" @query="search"></data-table>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: xs } = createNamespacedHelpers('xs');
+const { mapActions: kh } = createNamespacedHelpers('kh');
+const { mapActions: count } = createNamespacedHelpers('count');
+const { mapActions: khselect } = createNamespacedHelpers('khselect');
+export default {
+  metaInfo: { title: '销售员业绩明细' },
+  name: 'sjxsdeta',
+  props: {},
+  components: {
+    dataTable, //列表组件
+  },
+  data: function() {
+    return {
+      // 查询
+      searchForm: {},
+      xsList: [],
+      // 客户列表
+      opera: [],
+      fields: [
+        { label: '名字', prop: 'name' },
+        { label: '手机号', prop: 'tel' },
+      ],
+      list: [],
+      total: 0,
+      // 客户总数
+      khtotal: 0,
+    };
+  },
+  created() {
+    this.searchXs();
+  },
+  methods: {
+    ...xs({ xsQuery: 'query' }),
+    ...kh({ khQuery: 'query' }),
+    ...count({ countQuery: 'query' }),
+    ...khselect({ khselectQuery: 'khselectQuery' }),
+    async searchXs() {
+      let res = await this.xsQuery({ ssid: this.user.tableid });
+      if (this.$checkRes(res)) {
+        let data = res.data;
+        data.push({ name: '全部' });
+        this.$set(this, `xsList`, data);
+      }
+    },
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      info = {
+        id: this.searchForm.xsid,
+        tel: this.searchForm.tel,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+        needdata: true,
+      };
+      let res = await this.countQuery({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, _.get(res.data[0], 'list'));
+        this.$set(this, `total`, _.get(res.data[0], 'khtotal'));
+        this.$set(this, `khtotal`, _.get(res.data[0], 'khtotal'));
+      }
+    },
+    // 导出数据
+    async selectBtn() {
+      let info = {
+        xsid: this.searchForm.xsid,
+        tel: this.searchForm.tel,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+      };
+      let res = await this.khselectQuery({ ...info });
+      if (this.$checkRes(res)) {
+        window.open(res.data);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downList {
+    .downListOne {
+      padding: 15px 0;
+    }
+  }
+}
+</style>

+ 144 - 0
src/components/page/zhSign/fhSign.vue

@@ -0,0 +1,144 @@
+<template>
+  <div id="fhSign">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 分行客户报名统计 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="4">
+              <el-select v-model="searchForm.fhid" clearable placeholder="请选择分行" :popper-append-to-body="false">
+                <el-option v-for="item in fnList" :key="item.id" :label="item.name" :value="item.tableid"> </el-option>
+              </el-select>
+            </el-col>
+            <el-col :span="6">
+              <el-date-picker
+                v-model="searchForm.date"
+                value-format="yyyy-MM-dd"
+                format="yyyy-MM-dd"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                clearable
+              >
+              </el-date-picker>
+            </el-col>
+            <el-col :span="14">
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <el-col :span="24" class="downListOne">
+              <el-col :span="12"> 分行客户总数{{ khtotal }}人 </el-col>
+              <el-col :span="12" style="text-align:right;">
+                <el-button type="primary" size="mini" @click="selectBtn()">导出数据</el-button>
+              </el-col>
+            </el-col>
+            <el-col :span="24" class="downListTwo">
+              <data-table :fields="fields" :opera="opera" :data="list" :total="total" @query="search"> </data-table>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: sj } = createNamespacedHelpers('sj');
+const { mapActions: xs } = createNamespacedHelpers('xs');
+const { mapActions: kh } = createNamespacedHelpers('kh');
+const { mapActions: count } = createNamespacedHelpers('count');
+export default {
+  metaInfo: { title: '分行客户报名统计' },
+  name: 'fhSign',
+  props: {},
+  components: {
+    dataTable, //列表组件
+  },
+  data: function() {
+    return {
+      // 查询
+      searchForm: {},
+      fnList: [],
+      // 销售员列表
+      opera: [],
+      fields: [
+        { label: '销售员', prop: 'name' },
+        { label: '客户总数', prop: 'khtotal' },
+      ],
+      list: [],
+      total: 0,
+      khtotal: 0,
+    };
+  },
+  created() {
+    this.searchSj();
+  },
+  methods: {
+    ...sj(['query']),
+    ...xs({ xsQuery: 'query' }),
+    ...kh({ khQuery: 'query' }),
+    ...count({ countQuery: 'query', otherselectQuery: 'otherselectQuery' }),
+    // 查询分行列表
+    async searchSj() {
+      let res = await this.query({ js: '1' });
+      if (this.$checkRes(res)) {
+        let data = res.data;
+        data.push({ name: '全部' });
+        this.$set(this, `fnList`, data);
+      }
+    },
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      info = {
+        ssid: this.searchForm.fhid,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+      };
+      let res = await this.countQuery({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+        let khtotal = res.data.reduce((p, n) => p + (n['khtotal'] * 1 || 0), 0);
+        if (khtotal) this.$set(this, `khtotal`, khtotal);
+      }
+    },
+    async selectBtn() {
+      let info = {
+        ssid: this.searchForm.fhid,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+      };
+      let res = await this.otherselectQuery({ ...info });
+      if (this.$checkRes(res)) {
+        window.open(res.data);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downList {
+    .downListOne {
+      padding: 15px 0;
+    }
+  }
+}
+/deep/.el-select-dropdown__wrap {
+  max-height: 340px;
+}
+</style>

+ 142 - 0
src/components/page/zhSign/sjSign.vue

@@ -0,0 +1,142 @@
+<template>
+  <div id="sjSign">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 商家客户报名统计 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="4">
+              <el-select v-model="searchForm.sjid" clearable placeholder="请选择商家">
+                <el-option v-for="item in sjList" :key="item.id" :label="item.name" :value="item.tableid"> </el-option>
+              </el-select>
+            </el-col>
+            <el-col :span="6">
+              <el-date-picker
+                v-model="searchForm.date"
+                value-format="yyyy-MM-dd"
+                format="yyyy-MM-dd"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                clearable
+              >
+              </el-date-picker>
+            </el-col>
+            <el-col :span="14">
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <el-col :span="24" class="downListOne">
+              <el-col :span="12"> 商家客户总数{{ khtotal }}人 </el-col>
+              <el-col :span="12" style="text-align:right;">
+                <el-button type="primary" size="mini" @click="selectBtn()">导出数据</el-button>
+              </el-col>
+            </el-col>
+            <el-col :span="24" class="downListTwo">
+              <data-table :fields="fields" :opera="opera" :data="list" :total="total" @query="search"> </data-table>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: sj } = createNamespacedHelpers('sj');
+const { mapActions: xs } = createNamespacedHelpers('xs');
+const { mapActions: kh } = createNamespacedHelpers('kh');
+const { mapActions: count } = createNamespacedHelpers('count');
+export default {
+  metaInfo: { title: '商家客户报名统计' },
+  name: 'sjSign',
+  props: {},
+  components: {
+    dataTable, //列表组件
+  },
+  data: function() {
+    return {
+      // 查询
+      searchForm: {},
+      sjList: [],
+      // 客户列表
+      opera: [],
+      fields: [
+        { label: '销售员', prop: 'name' },
+        { label: '客户总数', prop: 'khtotal' },
+      ],
+      list: [],
+      total: 0,
+      // 客户总数
+      khtotal: 0,
+    };
+  },
+  created() {
+    this.searchSj();
+  },
+  methods: {
+    // 查询商家
+    ...sj({ sjQuery: 'query' }),
+    ...xs({ xsQuery: 'query' }),
+    ...kh({ khQuery: 'query' }),
+    ...count({ countQuery: 'query', otherselectQuery: 'otherselectQuery' }),
+    async searchSj() {
+      let res = await this.sjQuery({ js: '2' });
+      if (this.$checkRes(res)) {
+        let data = res.data;
+        data.push({ name: '全部' });
+        this.$set(this, `sjList`, data);
+      }
+    },
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      info = {
+        ssid: this.searchForm.sjid,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+      };
+      let res = await this.countQuery({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+        let khtotal = res.data.reduce((p, n) => p + (n['khtotal'] * 1 || 0), 0);
+        if (khtotal) this.$set(this, `khtotal`, khtotal);
+      }
+    },
+    async selectBtn() {
+      let info = {
+        ssid: this.searchForm.sjid,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+        table: 'xs',
+      };
+      let res = await this.otherselectQuery({ ...info });
+      if (this.$checkRes(res)) {
+        window.open(res.data);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downList {
+    .downListOne {
+      padding: 15px 0;
+    }
+  }
+}
+</style>

+ 124 - 0
src/components/page/zhSign/weixin.vue

@@ -0,0 +1,124 @@
+<template>
+  <div id="weixin">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="crumbs">
+          <el-breadcrumb separator="/">
+            <el-breadcrumb-item> <i class="el-icon-lx-cascades"></i> 微信宣传转发统计 </el-breadcrumb-item>
+          </el-breadcrumb>
+        </el-col>
+        <el-col :span="24" class="container down">
+          <el-col :span="24" class="downTop">
+            <el-col :span="4">
+              <el-select v-model="searchForm.xsid" clearable placeholder="请选择销售员">
+                <el-option v-for="item in xsList" :key="item.id" :label="item.name" :value="item.id"> </el-option>
+              </el-select>
+            </el-col>
+            <el-col :span="6">
+              <el-date-picker
+                v-model="searchForm.date"
+                value-format="yyyy-MM-dd"
+                format="yyyy-MM-dd"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                clearable
+              >
+              </el-date-picker>
+            </el-col>
+            <el-col :span="14">
+              <el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
+            </el-col>
+          </el-col>
+          <el-col :span="24" class="downList">
+            <el-col :span="24" class="downListOne"> 微信转发总{{ wxtotal }}次 </el-col>
+            <el-col :span="24" class="downListTwo">
+              <data-table :fields="fields" :opera="opera" :data="list" :total="total" @query="search">
+                <template #custom="{item, row}">
+                  <template v-if="item.prop == 'num'">
+                    <span>{{ getkhTotal(row) }}</span>
+                  </template>
+                </template>
+              </data-table>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import dataTable from '@/components/frame/filter-page-table.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: xs } = createNamespacedHelpers('xs');
+const { mapActions: zf } = createNamespacedHelpers('zf');
+export default {
+  metaInfo: { title: '微信宣传转发统计' },
+  name: 'weixin',
+  props: {},
+  components: {
+    dataTable, //列表组件
+  },
+  data: function() {
+    return {
+      // 查询
+      searchForm: {},
+      xsList: [],
+      // 客户列表
+      opera: [],
+      fields: [
+        { label: '销售员姓名', prop: 'xsname' },
+        { label: '微信转发数', prop: 'times' },
+      ],
+      list: [],
+      total: 0,
+      // 微信转发总数
+      wxtotal: 0,
+    };
+  },
+  created() {
+    this.searchXs();
+  },
+  methods: {
+    ...xs({ xsQuery: 'query' }),
+    ...zf({ zfQuery: 'query' }),
+    async searchXs() {
+      let res = await this.xsQuery({ ssid: this.user.tableid });
+      if (this.$checkRes(res)) {
+        let data = res.data;
+        data.push({ name: '全部' });
+        this.$set(this, `xsList`, data);
+      }
+    },
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      info = {
+        xsid: this.searchForm.xsid,
+        start: _.get(this.searchForm, 'date[0]'),
+        end: _.get(this.searchForm, 'date[1]'),
+      };
+      let res = await this.zfQuery({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        let wxtotal = res.data.reduce((p, n) => p + (n['times'] * 1 || 0), 0);
+        if (wxtotal) this.$set(this, `wxtotal`, wxtotal);
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.down {
+  .downList {
+    .downListOne {
+      padding: 15px 0;
+    }
+  }
+}
+</style>

+ 49 - 0
src/main.js

@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import App from './App.vue';
+import router from './router';
+import store from './store';
+import ElementUI from 'element-ui';
+import VueI18n from 'vue-i18n';
+import '@/plugins/axios';
+import '@/plugins/check-res';
+import '@/plugins/meta';
+import '@/plugins/filters';
+import '@/plugins/loading';
+import '@/plugins/var';
+import '@/plugins/methods';
+import '@/plugins/setting';
+import { messages } from './components/common/i18n';
+import 'element-ui/lib/theme-chalk/index.css'; // 默认主题
+// import './assets/css/theme-green/index.css'; // 浅绿色主题
+import './assets/css/icon.css';
+import './components/common/directives';
+import 'babel-polyfill';
+
+Vue.config.productionTip = false;
+Vue.use(VueI18n);
+Vue.use(ElementUI, {
+  size: 'small',
+});
+const i18n = new VueI18n({
+  locale: 'zh',
+  messages,
+});
+
+//使用钩子函数对路由进行权限跳转
+router.beforeEach((to, from, next) => {
+  document.title = `${to.meta.title} | ccb`;
+  const user = localStorage.getItem('user');
+  if (!user && to.path !== '/login') {
+    next('/login');
+  } else {
+    store.commit('setUser', JSON.parse(user), { root: true });
+    next();
+  }
+});
+
+new Vue({
+  router,
+  store,
+  i18n,
+  render: h => h(App),
+}).$mount('#app');

+ 19 - 0
src/plugins/axios.js

@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import AxiosWrapper from '@/util/axios-wrapper';
+
+const Plugin = {
+  install(vue, options) {
+    // 3. 注入组件
+    vue.mixin({
+      created() {
+        if (this.$store && !this.$store.$axios) {
+          this.$store.$axios = this.$axios;
+        }
+      },
+    });
+    // 4. 添加实例方法
+    vue.prototype.$axios = new AxiosWrapper(options);
+  },
+};
+
+Vue.use(Plugin, { baseUrl: process.env.VUE_APP_AXIOS_BASE_URL });

+ 39 - 0
src/plugins/check-res.js

@@ -0,0 +1,39 @@
+/* eslint-disable no-underscore-dangle */
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-unused-vars */
+/* eslint-disable no-shadow */
+import Vue from 'vue';
+import _ from 'lodash';
+import { Message } from 'element-ui';
+
+const vm = new Vue({});
+const Plugin = {
+  install(Vue, options) {
+    // 4. 添加实例方法
+    Vue.prototype.$checkRes = (res, okText, errText) => {
+      let _okText = okText;
+      let _errText = errText;
+      if (!_.isFunction(okText) && _.isObject(okText) && okText != null) {
+        ({ okText: _okText, errText: _errText } = okText);
+      }
+      const { errcode = 0, errmsg } = res || {};
+      if (errcode === 0) {
+        if (_.isFunction(_okText)) {
+          return _okText();
+        }
+        if (_okText) {
+          Message.success(_okText);
+        }
+        return true;
+      }
+      if (_.isFunction(_errText)) {
+        return _errText();
+      }
+      Message.error(_errText || errmsg);
+      Message({ message: _errText || errmsg, duration: 2000 });
+      return false;
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 5 - 0
src/plugins/element.js

@@ -0,0 +1,5 @@
+import Vue from 'vue';
+import Element from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+
+Vue.use(Element);

+ 6 - 0
src/plugins/filters.js

@@ -0,0 +1,6 @@
+import Vue from 'vue';
+import filters from '@/util/filters';
+
+for (const method in filters) {
+  Vue.filter(method, filters[method]);
+}

+ 27 - 0
src/plugins/loading.js

@@ -0,0 +1,27 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import Vue from 'vue';
+
+const Plugin = {
+  // eslint-disable-next-line no-unused-vars
+  install(vue, options) {
+    // 3. 注入组件
+    vue.mixin({
+      created() {
+        // eslint-disable-next-line no-underscore-dangle
+        const isRoot = this.constructor === Vue;
+        // console.log(`rootId:${rootVue_uid}; thisId:${this._uid}`);
+        // if (rootVue_uid !== 3) {
+        //   console.log(this);
+        // }
+        if (isRoot) {
+          const el = document.getElementById('loading');
+          if (el) el.style.display = 'none';
+        }
+      },
+    });
+  },
+};
+
+Vue.use(Plugin, { baseUrl: process.env.VUE_APP_AXIOS_BASE_URL });

+ 4 - 0
src/plugins/meta.js

@@ -0,0 +1,4 @@
+import Vue from 'vue';
+import Meta from 'vue-meta';
+
+Vue.use(Meta);

+ 33 - 0
src/plugins/methods.js

@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import _ from 'lodash';
+const Plugin = {
+  install(Vue, options) {
+    // 3. 注入组件
+    Vue.mixin({
+      created() {
+        if (this.$store && !this.$store.$toUndefined) {
+          this.$store.$toUndefined = this.$toUndefined;
+        }
+      },
+    });
+    // 4. 添加实例方法
+    Vue.prototype.$toUndefined = object => {
+      let keys = Object.keys(object);
+      keys.map(item => {
+        object[item] = object[item] === '' ? (object[item] = undefined) : object[item];
+      });
+      return object;
+    };
+    Vue.prototype.$turnTo = item => {
+      if (item.info_type == 1) {
+        window.open(item.url);
+      } else {
+        let router = window.vm.$router;
+        let route = window.vm.$route.path;
+        router.push({ path: `/info/detail?id=${item.id}` });
+      }
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 21 - 0
src/plugins/setting.js

@@ -0,0 +1,21 @@
+import Vue from 'vue';
+
+Vue.config.weixin = {
+  // baseUrl: process.env.BASE_URL + 'weixin',
+  // baseUrl: 'http://jh.tgoodsoft.net',
+  baseUrl: `http://${location.host}`,
+};
+
+Vue.config.stomp = {
+  // brokerURL: 'ws://http://free.liaoningdoupo.com/ws',
+  brokerURL: '/ws', // ws://${location.host}/ws
+  connectHeaders: {
+    host: 'visit',
+    login: 'visit', //visit
+    passcode: 'visit', //visit123
+  },
+  // debug: true,
+  reconnectDelay: 5000,
+  heartbeatIncoming: 4000,
+  heartbeatOutgoing: 4000,
+};

+ 65 - 0
src/plugins/stomp.js

@@ -0,0 +1,65 @@
+/**
+ * 基于WebStomp的消息处理插件
+ */
+
+import Vue from 'vue';
+import _ from 'lodash';
+import assert from 'assert';
+import { Client } from '@stomp/stompjs/esm5/client';
+
+const Plugin = {
+  install(Vue, options) {
+    assert(_.isObject(options));
+    if (options.debug && !_.isFunction(options.debug)) {
+      options.debug = str => {
+        console.log(str);
+      };
+    }
+    assert(_.isString(options.brokerURL));
+    if (!options.brokerURL.startsWith('ws://')) {
+      options.brokerURL = `ws://${location.host}${options.brokerURL}`;
+    }
+
+    // 3. 注入组件
+    Vue.mixin({
+      beforeDestroy: function() {
+        if (this.$stompClient) {
+          this.$stompClient.deactivate();
+          delete this.$stompClient;
+        }
+      },
+    });
+
+    // 4. 添加实例方法
+    Vue.prototype.$stomp = function(subscribes = {}) {
+      // connect to mq
+      const client = new Client(options);
+      client.onConnect = frame => {
+        // Do something, all subscribes must be done is this callback
+        // This is needed because this will be executed after a (re)connect
+        console.log('[stomp] connected');
+        Object.keys(subscribes)
+          .filter(p => _.isFunction(subscribes[p]))
+          .forEach(key => {
+            client.subscribe(key, subscribes[key]);
+          });
+      };
+
+      client.onStompError = frame => {
+        // Will be invoked in case of error encountered at Broker
+        // Bad login/passcode typically will cause an error
+        // Complaint brokers will set `message` header with a brief message. Body may contain details.
+        // Compliant brokers will terminate the connection after any error
+        console.log('Broker reported error: ' + frame.headers['message']);
+        console.log('Additional details: ' + frame.body);
+      };
+
+      client.activate();
+
+      this.$stompClient = client;
+    };
+  },
+};
+export default () => {
+  Vue.use(Plugin, Vue.config.stomp);
+};

+ 25 - 0
src/plugins/var.js

@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import _ from 'lodash';
+
+const getSiteId = () => {
+  let host = `${window.location.hostname}`; //`999991.smart.jilinjobswx.cn ${window.location.hostname}`
+  let schId;
+  host = host.replace('http://', '');
+  let arr = host.split('.');
+  if (arr.length > 0) {
+    schId = arr[0];
+    if (schId === 'smart') schId = 'master';
+    else `${schId}`.includes('localhost') || `${schId}`.includes('127.0.0.1') ? (schId = '99991') : '';
+    sessionStorage.setItem('schId', `${schId}`.includes('localhost') || `${schId}`.includes('127.0.0.1') ? '99991' : schId);
+  }
+  return schId;
+};
+const Plugin = {
+  install(vue, options) {
+    // 4. 添加实例方法
+    vue.prototype.$limit = 10;
+    vue.prototype.$site = getSiteId();
+  },
+};
+
+Vue.use(Plugin);

+ 118 - 0
src/router/index.js

@@ -0,0 +1,118 @@
+import Vue from 'vue';
+import Router from 'vue-router';
+
+Vue.use(Router);
+export default new Router({
+  mode: 'history',
+  base: process.env.NODE_ENV === 'development' ? '' : 'ccb',
+  routes: [
+    {
+      path: '/',
+      redirect: '/dashboard',
+    },
+    {
+      path: '/',
+      component: () => import('../components/common/Home.vue'),
+      meta: { title: '自述文件' },
+      children: [
+        {
+          path: '/dashboard',
+          component: () => import('../components/page/Dashboard.vue'),
+          meta: { title: '系统首页' },
+        },
+        // 系统设置(个人信息,修改密码,二维码查看,活动设置图[总行])
+        {
+          path: '/personal',
+          component: () => import('../components/page/setting/personal.vue'),
+          meta: { title: '个人信息维护' },
+        },
+        {
+          path: '/updatepwd',
+          component: () => import('../components/page/setting/updatepwd.vue'),
+          meta: { title: '修改密码' },
+        },
+        {
+          path: '/qrcode',
+          component: () => import('../components/page/setting/qrcode.vue'),
+          meta: { title: '二维码查看' },
+        },
+        {
+          path: '/hdimage',
+          component: () => import('../components/page/setting/hdimage.vue'),
+          meta: { title: '活动设置图' },
+        },
+        // 总行菜单
+        // 同级账号管理
+        {
+          path: '/peer',
+          component: () => import('../components/page/peer/index.vue'),
+          meta: { title: '同级账号管理' },
+        },
+        // 分行用户管理
+        {
+          path: '/branch',
+          component: () => import('../components/page/branch/index.vue'),
+          meta: { title: '分行用户管理' },
+        },
+        // 总行报名统计
+        {
+          path: '/fhSign',
+          component: () => import('../components/page/zhSign/fhSign.vue'),
+          meta: { title: '分行客户报名统计' },
+        },
+        {
+          path: '/sjSign',
+          component: () => import('../components/page/zhSign/sjSign.vue'),
+          meta: { title: '商家客户报名统计' },
+        },
+        {
+          path: '/weixin',
+          component: () => import('../components/page/zhSign/weixin.vue'),
+          meta: { title: '微信宣传转发统计' },
+        },
+        // 分行菜单
+        {
+          path: '/business',
+          component: () => import('../components/page/business/index.vue'),
+          meta: { title: '商家用户管理' },
+        },
+        {
+          path: '/sale',
+          component: () => import('../components/page/sale/index.vue'),
+          meta: { title: '销售员管理' },
+        },
+        // 分行报名统计
+        {
+          path: '/fhxsdeta',
+          component: () => import('../components/page/fhSign/fhxsdeta.vue'),
+          meta: { title: '销售员业绩明细' },
+        },
+        {
+          path: '/fhxsstat',
+          component: () => import('../components/page/fhSign/fhxsstat.vue'),
+          meta: { title: '销售员业绩统计' },
+        },
+        {
+          path: '/fhsjdeta',
+          component: () => import('../components/page/fhSign/fhsjdeta.vue'),
+          meta: { title: '商家报名统计' },
+        },
+        // 商家报名统计
+        {
+          path: '/sjxsdeta',
+          component: () => import('../components/page/sjSign/sjxsdeta.vue'),
+          meta: { title: '销售员业绩明细' },
+        },
+      ],
+    },
+    {
+      path: '/login',
+      component: () => import('../components/page/Login.vue'),
+      meta: { title: '登录' },
+    },
+    {
+      path: '*',
+      redirect: '/404',
+    },
+  ],
+});

+ 30 - 0
src/store/count.js

@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  countInfo: `/api/jh/v1/count`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.countInfo}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async otherselectQuery({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.countInfo}/excel`, payload);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 34 - 0
src/store/index.js

@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as ustate from './user/state';
+import * as umutations from './user/mutations';
+import login from './login';
+import sj from './sj';
+import kh from './kh';
+import xs from './xs';
+import xt from './xt';
+import zf from './zf';
+import user from './user';
+import password from './password';
+import count from './count';
+import khselect from './select/khselect';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+  state: { ...ustate },
+  mutations: { ...umutations },
+  actions: {},
+  modules: {
+    login,
+    sj,
+    kh,
+    xs,
+    xt,
+    zf,
+    user,
+    password,
+    count,
+    khselect,
+  },
+});

+ 43 - 0
src/store/kh.js

@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  khInfo: `/api/jh/v1/kh`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.khInfo}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.khInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.khInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.khInfo}/${id}`, data);
+    return res;
+  },
+
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.khInfo}/${payload}`);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 47 - 0
src/store/login.js

@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  loginInfo: `/api/jh/v1/login`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.loginInfo}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async login({ commit }, { user }) {
+    const res = await this.$axios.$post(`${api.loginInfo}`, user);
+    if (res.errcode === 0) {
+      localStorage.setItem('user', JSON.stringify(res.data));
+      commit('setUser', res.data, { root: true });
+    }
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.loginInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.loginInfo}/${id}`, data);
+    return res;
+  },
+
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.loginInfo}/${payload}`);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 43 - 0
src/store/password.js

@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  passwordInfo: `/api/jh/v1/password`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.passwordInfo}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.passwordInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.passwordInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.passwordInfo}`, payload);
+    return res;
+  },
+
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.passwordInfo}/${payload}`);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 22 - 0
src/store/select/khselect.js

@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  khselectInfo: `/api/jh/v1/kh/excel`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async khselectQuery({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.khselectInfo}`, payload);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 43 - 0
src/store/sj.js

@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  sjInfo: `/api/jh/v1/sj`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.sjInfo}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.sjInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.sjInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.sjInfo}/${id}`, data);
+    return res;
+  },
+
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.sjInfo}/${payload}`);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 43 - 0
src/store/user.js

@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  userInfo: `/api/jh/v1/user`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.userInfo}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.userInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.userInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.userInfo}/${id}`, data);
+    return res;
+  },
+
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.userInfo}/${payload}`);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 33 - 0
src/store/user/mutations.js

@@ -0,0 +1,33 @@
+export const setUser = (state, payload) => {
+  state.user = payload;
+  // let res = true;
+  // //登陆时
+  // if (payload) {
+  //   state.token = payload;
+  // } else {
+  //   //已经登陆,切换路由时取出用户信息放在总store中
+  //   let token = localStorage.getItem('token');
+  //   if (token && token !== 'guest') {
+  //     state.user = jwt.decode(token);
+  //   } else if (token && token == 'guest') {
+  //     let user = localStorage.getItem('user');
+  //     state.user = JSON.parse(user);
+  //   } else {
+  //     let timestamp = new Date().getTime();
+  //     let user = {
+  //       // id: `guest${timestamp}`,
+  //       name: `游客${timestamp}`,
+  //     };
+  //     state.user = user;
+  //     localStorage.setItem('token', 'guest');
+  //     localStorage.setItem('user', JSON.stringify(user));
+  //     console.warn('游客身份');
+  //   }
+  // }
+  // return res;
+};
+
+export const deleteUser = (state, payload) => {
+  state.user = {};
+  localStorage.removeItem('token');
+};

+ 2 - 0
src/store/user/state.js

@@ -0,0 +1,2 @@
+export const user = {};
+export const menuList = [];

+ 43 - 0
src/store/xs.js

@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  xsInfo: `/api/jh/v1/xs`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.xsInfo}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.xsInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.xsInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.xsInfo}/${id}`, data);
+    return res;
+  },
+
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.xsInfo}/${payload}`);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 43 - 0
src/store/xt.js

@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  xtInfo: `/api/jh/v1/xt`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.xtInfo}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.xtInfo}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.xtInfo}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.xtInfo}/${id}`, data);
+    return res;
+  },
+
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.xtInfo}/${payload}`);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 26 - 0
src/store/zf.js

@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+Vue.use(Vuex);
+const api = {
+  zfInfo: `/api/jh/v1/zf`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.zfInfo}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 117 - 0
src/util/axios-wrapper.js

@@ -0,0 +1,117 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _ from 'lodash';
+import Axios from 'axios';
+import { Util, Error } from 'naf-core';
+// import { Indicator } from 'mint-ui';
+import util from './user-util';
+
+const { trimData, isNullOrUndefined } = Util;
+const { ErrorCode } = Error;
+
+let currentRequests = 0;
+
+export default class AxiosWrapper {
+  constructor({ baseUrl = '', unwrap = true } = {}) {
+    this.baseUrl = baseUrl;
+    this.unwrap = unwrap;
+  }
+
+  // 替换uri中的参数变量
+  static merge(uri, query = {}) {
+    if (!uri.includes(':')) {
+      return uri;
+    }
+    const keys = [];
+    const regexp = /\/:([a-z0-9_]+)/gi;
+    let res;
+    // eslint-disable-next-line no-cond-assign
+    while ((res = regexp.exec(uri)) != null) {
+      keys.push(res[1]);
+    }
+    keys.forEach(key => {
+      if (!isNullOrUndefined(query[key])) {
+        uri = uri.replace(`:${key}`, query[key]);
+      }
+    });
+    return uri;
+  }
+
+  $get(uri, query, options) {
+    return this.$request(uri, null, query, options);
+  }
+
+  $post(uri, data = {}, query, options) {
+    return this.$request(uri, data, query, options);
+  }
+  $delete(uri, data = {}, router, query, options = {}) {
+    options = { ...options, method: 'delete' };
+    return this.$request(uri, data, query, options, router);
+  }
+  async $request(uri, data, query, options) {
+    // TODO: 合并query和options
+    if (_.isObject(query) && _.isObject(options)) {
+      options = { ...options, params: query, method: 'get' };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    if (!options) options = {};
+    if (options.params) options.params = trimData(options.params);
+    const url = AxiosWrapper.merge(uri, options.params);
+    currentRequests += 1;
+    // Indicator.open({
+    //   spinnerType: 'fading-circle',
+    // });
+
+    try {
+      const axios = Axios.create({
+        baseURL: this.baseUrl,
+      });
+      axios.defaults.headers.common.Authorization = util.token;
+      let res = await axios.request({
+        method: isNullOrUndefined(data) ? 'get' : 'post',
+        url,
+        data,
+        responseType: 'json',
+        ...options,
+      });
+      res = res.data;
+      const { errcode, errmsg, details } = res;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return res;
+      }
+      // unwrap data
+      if (this.unwrap) {
+        res = _.omit(res, ['errmsg', 'details']);
+        const keys = Object.keys(res);
+        if (keys.length === 1 && keys.includes('data')) {
+          res = res.data;
+        }
+      }
+      return res;
+    } catch (err) {
+      let errmsg = '接口请求失败,请稍后重试';
+      if (err.response) {
+        const { status } = err.response;
+        if (status === 401) errmsg = '用户认证失败,请重新登录';
+        if (status === 403) errmsg = '当前用户不允许执行该操作';
+      }
+      console.error(
+        `[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - 
+        ${err.message}`
+      );
+      return { errcode: ErrorCode.SERVICE_FAULT, errmsg, details: err.message };
+    } finally {
+      /* eslint-disable */
+      currentRequests -= 1;
+      if (currentRequests <= 0) {
+        currentRequests = 0;
+        // Indicator.close();
+      }
+    }
+  }
+}

+ 10 - 0
src/util/filters.js

@@ -0,0 +1,10 @@
+import _ from 'lodash';
+
+const filters = {
+  getName(object) {
+    const { data, searchItem } = object;
+    return _.get(data, searchItem) === undefined ? '' : _.get(data, searchItem);
+  },
+};
+
+export default filters;

+ 30 - 0
src/util/menus.js

@@ -0,0 +1,30 @@
+export const coreMenu = [
+  {
+    path: '/talk/list/in',
+    name: '校内宣讲会',
+  },
+  {
+    path: '/talk/list/out',
+    name: '校外宣讲会',
+  },
+  {
+    path: '/jobfair/list/in',
+    name: '校内招聘会',
+  },
+  {
+    path: '/jobfair/list/out',
+    name: '校外招聘会',
+  },
+  {
+    path: '/jobinfo/list',
+    name: '在线招聘',
+  },
+  {
+    path: '/jobs/list/official',
+    name: '正式岗位',
+  },
+  {
+    path: '/jobs/list/internship',
+    name: '实习岗位',
+  },
+];

+ 50 - 0
src/util/methods-util.js

@@ -0,0 +1,50 @@
+import { Util } from 'naf-core';
+
+const { isNullOrUndefined } = Util;
+
+export default {
+  //判断信息是否过期
+  isDateOff(dataDate) {
+    const now = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
+    dataDate = new Date(dataDate);
+    return now.getTime() <= dataDate.getTime();
+  },
+  //判断企业是否可以执行此动作/显示
+  checkCorp(data) {
+    const { role, unit, selfUnit, status, displayType, userid } = data;
+    if (!isNullOrUndefined(selfUnit) && !isNullOrUndefined(status)) {
+      return role === 'corp' && selfUnit === unit && status === '0';
+    } else if (!isNullOrUndefined(displayType)) {
+      if (role === 'corp') {
+        return role === displayType;
+      } else {
+        return role === displayType && !isNullOrUndefined(userid);
+      }
+    }
+  },
+  //获取url的参数params
+  getParams() {
+    let str = location.href;
+    let num = str.indexOf('?');
+    const param = {};
+    str = str.substr(num + 1);
+    let num2 = str.indexOf('#');
+    let str2 = '';
+    if (num2 > 0) {
+      str2 = str.substr(0, num2);
+    } else {
+      num2 = str.indexOf('/');
+      str2 = str.substr(0, num2);
+    }
+    const arr = str2.split('&');
+    for (let i = 0; i < arr.length; i++) {
+      num = arr[i].indexOf('=');
+      if (num > 0) {
+        const name = arr[i].substring(0, num);
+        const value = arr[i].substr(num + 1);
+        param[name] = decodeURI(value);
+      }
+    }
+    return param;
+  },
+};

+ 47 - 0
src/util/optionTitles.js

@@ -0,0 +1,47 @@
+export const JOBFAIR_TITLE = [
+  { prop: 'subject', label: '' },
+  { prop: 'address', label: '举办地址' },
+  { prop: 'date', label: '举办日期' },
+  { prop: 'unit', label: '分站信息' },
+];
+
+export const CAMPUS_TITLE = [
+  { prop: 'subject', label: '' },
+  { prop: 'address', label: '举办地址' },
+  { prop: 'status', label: '审核状态' },
+  { prop: 'date', label: '举办日期' },
+  { prop: 'unit', label: '分站信息' },
+];
+
+export const JOBINFO_TITLE = [
+  { prop: 'title', label: '' },
+  { prop: 'count', label: '需求人数' },
+  { prop: 'nature.name', label: '工作性质' },
+  { prop: 'salary.name', label: '薪资待遇' },
+  { prop: 'xlreqs.name', label: '最低学历' },
+  { prop: 'city.name', label: '所在城市' },
+  // { prop: 'expired', label: '状态' },
+];
+
+export const RESUME_TITLE = [{ prop: 'title', label: '' }];
+
+export const LETTER_TITLE = [
+  { prop: 'title', label: '' },
+  { prop: 'corpname', label: '企业名称' },
+  { prop: 'type', label: '类型' },
+  { prop: 'status', label: '状态' },
+];
+
+export const TICKET_TITLE = [
+  { prop: 'subject', label: '' },
+  { prop: 'type', label: '门票类型' },
+  { prop: 'origin', label: '' },
+  { prop: 'date', label: '举办日期' },
+];
+
+export const CORP_JOBFAIR = [
+  { prop: 'subject', label: '' },
+  { prop: 'time', label: '举办时间' },
+  { prop: 'date', label: '举办日期' },
+  { prop: 'unit', label: '分站信息' },
+];

+ 69 - 0
src/util/user-util.js

@@ -0,0 +1,69 @@
+/* eslint-disable no-console */
+export default {
+  get user() {
+    const val = sessionStorage.getItem('user');
+    try {
+      if (val) return JSON.parse(val);
+    } catch (err) {
+      console.error(err);
+    }
+    return null;
+  },
+  set user(userinfo) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+  },
+  get token() {
+    return sessionStorage.getItem('token');
+  },
+  set token(token) {
+    sessionStorage.setItem('token', token);
+  },
+  get openid() {
+    return sessionStorage.getItem('openid');
+  },
+  set openid(openid) {
+    sessionStorage.setItem('openid', openid);
+  },
+  get isGuest() {
+    return !this.user || this.user.role === 'guest';
+  },
+  save({ userinfo, token }) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+    sessionStorage.setItem('token', token);
+  },
+
+  get corpInfo() {
+    const val = sessionStorage.getItem('corpInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set corpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+  saveCorpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+
+  get unit() {
+    const val = sessionStorage.getItem('unit');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set unit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  saveUnit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  get userInfo() {
+    const val = sessionStorage.getItem('userInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set userInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+  saveUserInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+};

+ 35 - 0
vue.config.js

@@ -0,0 +1,35 @@
+const path = require('path');
+module.exports = {
+  publicPath: process.env.NODE_ENV === 'development' ? '/' : process.env.VUE_APP_ROUTER,
+  // 打包文件
+  outputDir: 'adminsys',
+  configureWebpack: config => {
+    Object.assign(config, {
+      // 开发生产共同配置
+      resolve: {
+        alias: {
+          '@': path.resolve(__dirname, './src'),
+          '@c': path.resolve(__dirname, './src/components'),
+          '@a': path.resolve(__dirname, './src/assets'),
+        },
+      },
+    });
+  },
+  devServer: {
+    port: '8001',
+    //api地址前缀
+    proxy: {
+      '/files': {
+        target: 'http://jh.tgoodsoft.net',
+        pathRewrite: {
+          '^/files': '',
+        },
+      },
+      '/api': {
+        target: 'http://jh.tgoodsoft.net',
+        changeOrigin: true,
+        ws: true,
+      },
+    },
+  },
+};