asd123a20 4 gadi atpakaļ
vecāks
revīzija
c74d2eadfe
100 mainītis faili ar 9862 papildinājumiem un 0 dzēšanām
  1. 20 0
      admin-frame/.env
  2. 4 0
      admin-frame/.env.51
  3. 10 0
      admin-frame/.env.54
  4. 9 0
      admin-frame/.env.54d
  5. 3 0
      admin-frame/.env.hj
  6. 7 0
      admin-frame/.eslintignore
  7. 33 0
      admin-frame/.eslintrc.js
  8. 22 0
      admin-frame/.gitignore
  9. 8 0
      admin-frame/.prettierrc
  10. 3 0
      admin-frame/README.md
  11. 12 0
      admin-frame/babel.config.js
  12. 20 0
      admin-frame/config/index.js
  13. 3 0
      admin-frame/lib/layouts/README.md
  14. 33 0
      admin-frame/lib/layouts/bread.vue
  15. 137 0
      admin-frame/lib/layouts/frame.vue
  16. 36 0
      admin-frame/lib/layouts/litebar.vue
  17. 43 0
      admin-frame/lib/layouts/logo.vue
  18. 40 0
      admin-frame/lib/layouts/user.vue
  19. 1 0
      admin-frame/lib/plugins/README.md
  20. 22 0
      admin-frame/lib/plugins/axios.js
  21. 40 0
      admin-frame/lib/plugins/check-res.js
  22. 6 0
      admin-frame/lib/plugins/element.js
  23. 4 0
      admin-frame/lib/plugins/meta.js
  24. 26 0
      admin-frame/lib/plugins/naf-dict.js
  25. 28 0
      admin-frame/lib/plugins/naf-policy.js
  26. 31 0
      admin-frame/lib/plugins/power.js
  27. 65 0
      admin-frame/lib/plugins/stomp.js
  28. 21 0
      admin-frame/lib/store/naf/.dict.js
  29. 95 0
      admin-frame/lib/store/naf/dict.js
  30. 9 0
      admin-frame/lib/store/naf/index.js
  31. 48 0
      admin-frame/lib/style/drawer.less
  32. 68 0
      admin-frame/lib/style/index.less
  33. 56 0
      admin-frame/lib/style/lite.less
  34. 50 0
      admin-frame/lib/style/mixed.less
  35. 132 0
      admin-frame/lib/utils/axios-wrapper.js
  36. 18 0
      admin-frame/lib/utils/filters.js
  37. 124 0
      admin-frame/lib/utils/store.js
  38. 77 0
      admin-frame/lib/utils/user-util.js
  39. 25 0
      admin-frame/mock/menu.54/app.js
  40. 20 0
      admin-frame/mock/menu.54/authorize.js
  41. 36 0
      admin-frame/mock/menu.54/gaf.js
  42. 86 0
      admin-frame/mock/menu.54/index.js
  43. 15 0
      admin-frame/mock/menu.54/link.js
  44. 37 0
      admin-frame/mock/menu.54/user.js
  45. 25 0
      admin-frame/mock/menu/app.js
  46. 25 0
      admin-frame/mock/menu/authorize.js
  47. 50 0
      admin-frame/mock/menu/cred.js
  48. 30 0
      admin-frame/mock/menu/dev.js
  49. 50 0
      admin-frame/mock/menu/gaf.js
  50. 140 0
      admin-frame/mock/menu/index.js
  51. 45 0
      admin-frame/mock/menu/journal.js
  52. 15 0
      admin-frame/mock/menu/link.js
  53. 53 0
      admin-frame/mock/menu/log.js
  54. 20 0
      admin-frame/mock/menu/policy.js
  55. 25 0
      admin-frame/mock/menu/register.js
  56. 14 0
      admin-frame/mock/menu/terminal.js
  57. 45 0
      admin-frame/mock/menu/user.js
  58. 137 0
      admin-frame/naf/chart/tendency.vue
  59. 87 0
      admin-frame/naf/data/README.md
  60. 66 0
      admin-frame/naf/data/code-select.vue
  61. 96 0
      admin-frame/naf/data/demo-table.vue
  62. 145 0
      admin-frame/naf/data/filter-box.vue
  63. 189 0
      admin-frame/naf/data/filter-grid.vue
  64. 102 0
      admin-frame/naf/data/form-dlg.vue
  65. 143 0
      admin-frame/naf/data/form.vue
  66. 143 0
      admin-frame/naf/data/lite-grid.vue
  67. 141 0
      admin-frame/naf/data/meta-util.js
  68. 114 0
      admin-frame/naf/data/naf-input.vue
  69. 105 0
      admin-frame/naf/data/naf-table.vue
  70. 79 0
      admin-frame/naf/data/wang-editor.vue
  71. 12 0
      admin-frame/naf/error/403.vue
  72. 12 0
      admin-frame/naf/error/404.vue
  73. 12 0
      admin-frame/naf/error/500.vue
  74. 98 0
      admin-frame/naf/error/error-page.vue
  75. 40 0
      admin-frame/naf/frame/bread.vue
  76. 30 0
      admin-frame/naf/frame/footer.vue
  77. 123 0
      admin-frame/naf/frame/header/header-dropdown.vue
  78. 50 0
      admin-frame/naf/frame/header/index.vue
  79. 32 0
      admin-frame/naf/frame/header/litebar.vue
  80. 39 0
      admin-frame/naf/frame/header/litebar.vue.bak
  81. 60 0
      admin-frame/naf/frame/header/logo.vue
  82. 61 0
      admin-frame/naf/frame/header/logo.vue.bak
  83. 46 0
      admin-frame/naf/frame/header/navbar.vue
  84. 60 0
      admin-frame/naf/frame/header/navbar.vue.bak
  85. 213 0
      admin-frame/naf/frame/header/user.vue
  86. 77 0
      admin-frame/naf/frame/menu-item.vue
  87. 68 0
      admin-frame/naf/frame/sider.vue
  88. 32 0
      admin-frame/naf/layouts/footer-page.vue
  89. 31 0
      admin-frame/naf/layouts/scroll-page.vue
  90. 85 0
      admin-frame/naf/user/dept-select.vue
  91. 146 0
      admin-frame/naf/user/dept-tree.vue
  92. 150 0
      admin-frame/naf/user/user-select.vue
  93. 58 0
      admin-frame/package.json
  94. 0 0
      admin-frame/public/.gitkeep
  95. BIN
      admin-frame/public/favicon.ico
  96. 19 0
      admin-frame/public/index.html
  97. 539 0
      admin-frame/public/naf-icons/demo.css
  98. 850 0
      admin-frame/public/naf-icons/demo_fontclass.html
  99. 3252 0
      admin-frame/public/naf-icons/demo_index.html
  100. 0 0
      admin-frame/public/naf-icons/demo_symbol.html

+ 20 - 0
admin-frame/.env

@@ -0,0 +1,20 @@
+VUE_APP_AXIOS_BASE_URL=/api
+VUE_APP_ROUTER_MODE=history
+VUE_APP_ROOT_URL=/admin/
+VUE_APP_PRODUCT_NAME=信任管理系统
+VUE_APP_SHORT_NAME=信任管理系统
+VUE_APP_PRODUCT_LOGO=static/logo-xr.png
+VUE_APP_SHORT_LOGO=false
+# nested-两层嵌套导航(导航条+导航树); lite-单一树状导航, liteOnly, nestedOnly
+VUE_APP_MENU_MODE=liteOnly
+VUE_APP_VERIFY_CODE=true
+VUE_APP_FILTER_MULTI=true
+# 查询匹配模式(默认为exact):exact-精确查询;like-模糊查询; all-支持两种模式。
+VUE_APP_FILTER_MATCHER=all
+VUE_APP_FILTER_MATCHER_DEFAULT=like
+VUE_APP_VERSION=1.0.0820.1
+VUE_APP_HOME_ROUTE=/
+VUE_APP_HEIGHT=937px
+VUE_APP_WIDTH=1920px
+VUE_APP_ROUTER_HOME=true
+VUE_APP_USER_SCOPE=true

+ 4 - 0
admin-frame/.env.51

@@ -0,0 +1,4 @@
+VUE_APP_PRODUCT_NAME=信任管理系统
+VUE_APP_SHORT_NAME=信任管理系统
+VUE_APP_PRODUCT_LOGO=static/logo-xr.png
+VUE_APP_HOME_ROUTE=/frame/@xms/xms/dashboard

+ 10 - 0
admin-frame/.env.54

@@ -0,0 +1,10 @@
+NODE_ENV=production
+VUE_APP_VERIFY_CODE=true
+VUE_APP_PRODUCT_NAME=用户及资源授权管理系统
+VUE_APP_SHORT_NAME=用户及资源授权管理
+VUE_APP_PRODUCT_LOGO=static/logo-ur.png
+VUE_APP_MOCK_MENU=./mock/menu.54
+VUE_APP_HOME_ROUTE=/
+VUE_APP_SHOW_VERSION=false
+VUE_APP_ROUTER_HOME=false
+VUE_APP_FILTER_MULTI=true

+ 9 - 0
admin-frame/.env.54d

@@ -0,0 +1,9 @@
+NODE_ENV=development
+VUE_APP_VERIFY_CODE=true
+VUE_APP_PRODUCT_NAME=用户及资源授权管理系统
+VUE_APP_SHORT_NAME=用户及资源授权管理
+VUE_APP_PRODUCT_LOGO=static/logo-ur.png
+VUE_APP_MOCK_MENU=./mock/menu.54
+VUE_APP_SHOW_VERSION=false
+VUE_APP_ROUTER_HOME=false
+VUE_APP_FILTER_MULTI=true

+ 3 - 0
admin-frame/.env.hj

@@ -0,0 +1,3 @@
+VUE_APP_PRODUCT_NAME=标识管理系统
+VUE_APP_SHORT_NAME=标识管理系统
+VUE_APP_PRODUCT_LOGO=static/logo-id.png

+ 7 - 0
admin-frame/.eslintignore

@@ -0,0 +1,7 @@
+/build/
+/build_bak/
+/config/
+/dist/
+/bak/
+/static/
+/node_modules/

+ 33 - 0
admin-frame/.eslintrc.js

@@ -0,0 +1,33 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  env: {
+    node: true,
+  },
+  extends: ['eslint:recommended', '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',
+  },
+};

+ 22 - 0
admin-frame/.gitignore

@@ -0,0 +1,22 @@
+.DS_Store
+node_modules
+/dist
+package-lock.json
+
+# 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*

+ 8 - 0
admin-frame/.prettierrc

@@ -0,0 +1,8 @@
+{
+  "trailingComma": "es5",
+  "tabWidth": 2,
+  "printWidth": 140,
+  "semi": true,
+  "singleQuote": true,
+  "bracketSpacing": true
+}

+ 3 - 0
admin-frame/README.md

@@ -0,0 +1,3 @@
+# admin-frame
+
+管理系统框架 & 公共组件库

+ 12 - 0
admin-frame/babel.config.js

@@ -0,0 +1,12 @@
+module.exports = {
+  presets: ['@vue/app'],
+  plugins: [
+    // [
+    //   'component',
+    //   {
+    //     libraryName: 'element-ui',
+    //     styleLibraryName: 'theme-chalk',
+    //   },
+    // ],
+  ],
+};

+ 20 - 0
admin-frame/config/index.js

@@ -0,0 +1,20 @@
+export default {
+  productName: process.env.VUE_APP_PRODUCT_NAME,
+  shortName: process.env.VUE_APP_SHORT_NAME,
+  rootName: '所有部门',
+  description: '提供用户、角色、权限等基本管理功能,为业务系统提供基础业务支撑。',
+  copyright: '长春吉大正元信息技术有限公司 版权所有 © 2019 ',
+  logo1: process.env.VUE_APP_PRODUCT_LOGO, // 登录页LOGO
+  logo2: false, // 主页LOGO
+  layout: {
+    breadHeight: '24px',
+    headerHeight: '36px',
+    footerHeight: '48px',
+    asideExpandWidth: '256px',
+    asideCollapseWidth: '64px',
+  },
+  pageSize: 10,
+  menuMode: process.env.VUE_APP_MENU_MODE, // nested-两层嵌套导航(导航条+导航树); lite-单一树状导航, liteOnly, nestedOnly
+  verifyCode: eval(process.env.VUE_APP_VERIFY_CODE), // 是否使用图片验证码
+  homeRoute: process.env.VUE_APP_HOME_ROUTE || '/', // 首页导航路由
+};

+ 3 - 0
admin-frame/lib/layouts/README.md

@@ -0,0 +1,3 @@
+# 框架布局组件
+## 针对单一应用模块的布局框架
+## 包含页面头和左侧菜单

+ 33 - 0
admin-frame/lib/layouts/bread.vue

@@ -0,0 +1,33 @@
+<template>
+  <el-breadcrumb separator="/">
+    <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+    <el-breadcrumb-item v-for="(item, index) in catalog" :key="index">{{ item }}</el-breadcrumb-item>
+  </el-breadcrumb>
+</template>
+<script>
+const DeepFind = (menus, path) => {
+  for (const k in menus) {
+    const item = menus[k];
+    //if (item.options.path == path) return menus[k];
+    if (path.endsWith(item.options.path)) return menus[k];
+    if (item.children) {
+      const res = DeepFind(item.children, path);
+      if (res) return res;
+    }
+  }
+  return false;
+};
+export default {
+  computed: {
+    catalog() {
+      // console.log(this.$route)
+      // return this.$route.meta.catalog;
+      const menu = DeepFind(this.menuItems, this.$route.path);
+      return (menu && menu.options && menu.options.meta && menu.options.meta.catalog) || [];
+    },
+    menuItems() {
+      return this.$store.state.menu.items;
+    },
+  },
+};
+</script>

+ 137 - 0
admin-frame/lib/layouts/frame.vue

@@ -0,0 +1,137 @@
+<template>
+  <el-container class="layout" direction="vertical">
+    <el-header class="header" :height="layout.headerHeight" :style="{ lineHeight: layout.headerHeight }">
+      <div class="header-box">
+        <naf-logo :width="asideWidth" :shortName="shortName" />
+        <naf-lite-bar :menu-collapse="menuCollapse" @toggle-menu="toggleMenu" />
+      </div>
+    </el-header>
+    <el-main style="padding: 0;display: flex;">
+      <el-container class="main">
+        <el-aside :width="asideWidth" class="sider" v-show="asideShow">
+          <naf-sider :menu-items="menuItems" :style="{ width: asideWidth }" theme="light" :is-collapse="menuCollapse" />
+        </el-aside>
+        <el-main class="content">
+          <div class="bread" :height="layout.breadHeight" v-show="asideShow">
+            <naf-bread></naf-bread>
+          </div>
+          <div class="page" ref="pageContainer">
+            <el-alert :title="errMsg" type="info" :description="errDesc" show-icon v-if="showError"> </el-alert>
+            <router-view v-else />
+          </div>
+        </el-main>
+      </el-container>
+    </el-main>
+  </el-container>
+</template>
+
+<script>
+import config from '@frame/config';
+import NafSider from '@naf/frame/sider';
+import NafLogo from './logo';
+import NafLiteBar from './litebar';
+import NafBread from './bread';
+
+const defaultConfig = {
+  breadHeight: '24px',
+  headerHeight: '64px',
+  footerHeight: '48px',
+  asideExpandWidth: '256px',
+  asideCollapseWidth: '64px',
+};
+
+const { layout = {} } = config;
+
+export default {
+  name: 'Frame',
+  components: {
+    NafSider,
+    NafLogo,
+    NafLiteBar,
+    NafBread,
+  },
+  props: {
+    menuItems: { type: Array, required: true },
+    shortName: { type: String, default: '系统管理' },
+    // menuCollapse: { type: Boolean, default: false },
+  },
+  data() {
+    return {
+      layout: { ...defaultConfig, ...layout },
+      showError: false,
+      errMsg: '',
+      errDesc: '',
+      menuCollapse: false,
+    };
+  },
+  methods: {
+    toggleMenu() {
+      this.menuCollapse = !this.menuCollapse;
+    },
+  },
+  computed: {
+    asideWidth() {
+      return this.menuCollapse ? layout.asideCollapseWidth : layout.asideExpandWidth;
+    },
+    asideShow() {
+      return true;
+    },
+  },
+  created() {
+    // eslint-disable-next-line no-undef
+    // if (!dd) {
+    //   throw new BusinessError();
+    // }
+  },
+  errorCaptured(err, vm, info) {
+    this.showError = true;
+    this.errMsg = err.message || '处理发生错误';
+    this.errDesc = err.details || info || '页面加载过程中发生错误';
+  },
+};
+</script>
+<style scoped lang="less">
+.layout {
+  height: 100%;
+  width: 100%;
+  overflow: hidden;
+}
+.header {
+  padding: 0;
+  .header-box {
+    background: #20a0ff;
+    color: #fff;
+    display: flex;
+    flex-direction: row;
+    padding: 0;
+    height: 100%;
+    width: 100%;
+  }
+}
+.sider {
+  background: #00b1ff;
+  height: 100%;
+  overflow-y: auto;
+  overflow-x: hidden;
+  border-right: solid 1px #e6e6e6;
+}
+.main {
+  // FOR EDGE
+  overflow: hidden;
+}
+.content {
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  padding: 0;
+  .bread {
+    padding: 10px 20px;
+  }
+  .page {
+    flex: 1;
+    overflow: auto;
+    display: block;
+    padding: 10px;
+  }
+}
+</style>

+ 36 - 0
admin-frame/lib/layouts/litebar.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="banner">
+    <i class="naf-icons" :class="{ 'naf-icon-unfold': menuCollapse, 'naf-icon-fold': !menuCollapse }" @click="toggleMenu"></i>
+    <naf-user class="right" />
+  </div>
+</template>
+<script>
+import NafUser from './user';
+
+export default {
+  components: {
+    NafUser,
+  },
+  props: {
+    menuCollapse: Boolean,
+  },
+  methods: {
+    toggleMenu() {
+      this.$emit('toggle-menu');
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.banner {
+  // flex: 1;
+  // display: block;
+  // align-items: center;
+  padding: 0 20px;
+  width: 58%;
+  clear: both;
+  .right {
+    float: right;
+  }
+}
+</style>

+ 43 - 0
admin-frame/lib/layouts/logo.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="logo" :style="{ width: width }">
+    <router-link to="/">
+      <img src="@/assets/logo1.svg" alt="logo" style="height:32px;width:32px;" />
+    </router-link>
+    <h1>{{ shortName }}</h1>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    shortName: String,
+    width: {
+      type: String,
+      default: '256px',
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.logo {
+  display: table;
+  box-sizing: border-box;
+  border-right: solid 1px #e6e6e6;
+  padding: 0 16px;
+  overflow: hidden;
+  img,
+  h1 {
+    vertical-align: middle;
+  }
+  h1 {
+    display: inline-block;
+    margin: 0 0 0 12px;
+    font-size: 1.3em;
+  }
+
+  .el-switch {
+    vertical-align: middle;
+    text-align: right;
+    display: table-cell;
+  }
+}
+</style>

+ 40 - 0
admin-frame/lib/layouts/user.vue

@@ -0,0 +1,40 @@
+<template>
+  <!--简洁用户菜单-->
+  <div class="right lite">
+    <span class="name">{{ (userinfo && userinfo.name) || '管理員' }}</span>
+  </div>
+</template>
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+  props: {
+    menuCollapse: Boolean,
+  },
+  methods: {
+    ...mapActions({
+      logout: 'login/logout',
+    }),
+    async handleLogout() {
+      // console.log(this.userinfo);
+      // console.log(this);
+      const res = await this.logout();
+      // console.log(res);
+      // if (!res.errcode) {
+      //   this.$router.push(this.$route.query.redirect || '/login');
+      // }
+    },
+  },
+  computed: {
+    ...mapGetters(['userinfo']),
+  },
+};
+</script>
+<style lang="less" scoped>
+.right.lite {
+  font-size: 14px;
+}
+.el-button--text {
+  color: white;
+}
+</style>

+ 1 - 0
admin-frame/lib/plugins/README.md

@@ -0,0 +1 @@
+### 框架使用的 vue plugin

+ 22 - 0
admin-frame/lib/plugins/axios.js

@@ -0,0 +1,22 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import Vue from 'vue';
+import AxiosWrapper from '../utils/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, unwrap: true });

+ 40 - 0
admin-frame/lib/plugins/check-res.js

@@ -0,0 +1,40 @@
+/* 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);
+          // Message({ message: _okText, type: 'success', duration: 60000 });
+        }
+        return true;
+      }
+      if (_.isFunction(_errText)) {
+        return _errText();
+      }
+      Message.error(_errText || errmsg);
+      // Message({ message: _errText || errmsg, duration: 60000 });
+      return false;
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 6 - 0
admin-frame/lib/plugins/element.js

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

+ 4 - 0
admin-frame/lib/plugins/meta.js

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

+ 26 - 0
admin-frame/lib/plugins/naf-dict.js

@@ -0,0 +1,26 @@
+/**
+ * 字典数据处理插件
+ */
+
+import Vue from 'vue';
+import _ from 'lodash';
+import assert from 'assert';
+
+const Plugin = {
+  install(vue, options) {
+    // 4. 添加实例方法
+    vue.prototype.$dict = function(codeType, code) {
+      assert(_.isString(codeType));
+      const state = this.$store.state.naf.dict;
+      if (!state) {
+        throw new Error("can't find store for naf dict");
+      }
+      if (_.isString(code)) {
+        return (state.codes[codeType] && state.codes[codeType][code]) || code;
+      } else {
+        return state.items[codeType];
+      }
+    };
+  },
+};
+Vue.use(Plugin);

+ 28 - 0
admin-frame/lib/plugins/naf-policy.js

@@ -0,0 +1,28 @@
+/**
+ * 字典数据处理插件
+ */
+
+import Vue from 'vue';
+import _ from 'lodash';
+import assert from 'assert';
+
+const Plugin = {
+  install(vue, options) {
+    // 4. 添加实例方法
+    vue.prototype.$policy = function(key) {
+      assert(_.isString(key));
+      const state = this.$store.state.naf.policy;
+      if (!state) {
+        throw new Error("can't find store for naf policy");
+      }
+      for (const i in state.items) {
+        if (state.items[i].key == key) {
+          return state.items[i].value;
+        } else {
+          return 0;
+        }
+      }
+    };
+  },
+};
+Vue.use(Plugin);

+ 31 - 0
admin-frame/lib/plugins/power.js

@@ -0,0 +1,31 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import Vue from 'vue';
+import { Message } from 'element-ui';
+const Plugin = {
+  install(vue, options) {
+    vue.prototype.$power = org => {
+      const orgId = window.sessionStorage.getItem('orgId');
+      const roles = window.sessionStorage.getItem('roles');
+      const role = JSON.parse(roles).filter(p => p == 'superadmin' || p == 'admin');
+      if (role.length <= 0) {
+        if (!orgId) {
+          Message.error('没有权限');
+          return false;
+        }
+        if (!org || !org.orgId || org.orgId == null) {
+          Message.error('没有权限');
+          return false;
+        }
+        if (!org.orgId.startsWith(orgId)) {
+          Message.error('没有权限');
+          return false;
+        }
+      }
+      return true;
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 65 - 0
admin-frame/lib/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);
+};

+ 21 - 0
admin-frame/lib/store/naf/.dict.js

@@ -0,0 +1,21 @@
+// mutation types
+export const LOADED = 'LOADED';
+
+export const PRESET = {
+  xb: [{ code: '男', name: '男' }, { code: '女', name: '女' }],
+  // org: [{ code: '0', name: '军' }, { code: '1', name: '民' }],
+  // reg: [{ code: '0', name: '待审核' }, { code: '1', name: '已审核' }, { code: '2', name: '已拒绝' }],
+  // device: [{ code: '0', name: '正常' }, { code: '1', name: '挂失' }, { code: '2', name: '损毁' }],
+  // status: [{ code: '0', name: '正常' }, { code: '1', name: '注销' }, { code: '2', name: '冻结' }],
+  // usage: [{ code: '0', name: '正常' }, { code: '1', name: '停用' }],
+  // role: [{ code: '0', name: '普通角色' }, { code: '1', name: '组合角色' }],
+  // cred: [
+  //   { code: 'fingerprint', name: '指纹' },
+  //   { code: 'fingervein', name: '指静脉' },
+  //   { code: 'iris', name: '虹膜' },
+  //   { code: 'face', name: '人脸' },
+  //   { code: 'token', name: '动态口令' },
+  //   { code: 'ukey', name: '证书(UKey)' },
+  //   { code: 'manual', name: '演示凭证(手工输入)' },
+  // ],
+};

+ 95 - 0
admin-frame/lib/store/naf/dict.js

@@ -0,0 +1,95 @@
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-shadow */
+// import * as types from './.dict.js';
+import assert from 'assert';
+import _ from 'lodash';
+import { LOADED, PRESET } from './.dict';
+
+const api = {
+  listItem: '/gaf/code/:codeType/items',
+  listUnit: '/naf/unit/list',
+  listXzqh: '/naf/code/xzqh/list',
+};
+
+// initial state
+export const state = () => ({
+  codeTypes: [], // 字典分类
+  items: {
+    usage: [
+      { code: '0', name: '正常' },
+      { code: '1', name: '停用' },
+    ],
+  },
+  codes: {
+    usage: {
+      0: '正常',
+      1: '停用',
+    },
+  },
+});
+
+// actions
+export const actions = {
+  async load({ state, commit }, payload) {
+    assert(payload);
+
+    if (state.items[payload]) {
+      return state.items[payload];
+    }
+
+    // LOAD PRESET DICT
+    if (PRESET[payload]) {
+      commit(LOADED, { codeType: payload, items: PRESET[payload] });
+      return PRESET[payload];
+    }
+
+    let res;
+    if (payload === 'unit') {
+      // LOAD UNIT DICT
+      res = await this.$axios.$get(api.listUnit);
+    } else if (payload === 'xzqh') {
+      // LOAD XZQH DICT
+      res = await this.$axios.$get(api.listXzqh, { level: 1 });
+      if (res.errcode) return res;
+      const rs1 = res.data || res;
+      res = await this.$axios.$get(api.listXzqh, { level: 2 });
+      if (res.errcode) return res;
+      const rs2 = res.data || res;
+      res = rs1.map(p => {
+        const prefix = p.code.substr(0, 2);
+        const children = rs2.filter(c => c.code !== p.code && c.code.startsWith(prefix));
+        return { ...p, children };
+      });
+    } else if (payload === 'city') {
+      // 吉林省内地市
+      res = await this.$axios.$get(api.listXzqh, { level: 2, parent: '220000' });
+    } else {
+      // LOAD COMMONS DICT
+      res = await this.$axios.$get(api.listItem, { codeType: payload });
+    }
+
+    if (!res.errcode) {
+      commit(LOADED, { codeType: payload, items: res.data || res });
+      return res.data || res;
+    }
+    return res;
+  },
+};
+
+// mutations
+export const mutations = {
+  [LOADED](state, { codeType, items }) {
+    state.items[codeType] = items;
+    state.codes[codeType] = items.reduce((acc, item) => {
+      acc[item.code] = item.name;
+      if (Array.isArray(item.children) && item.children.length > 0) {
+        _.forEach(item.children, p => {
+          acc[p.code] = p.name;
+        });
+      }
+      return acc;
+    }, {});
+  },
+};
+
+export const namespaced = true;

+ 9 - 0
admin-frame/lib/store/naf/index.js

@@ -0,0 +1,9 @@
+import * as dict from './dict';
+import * as policy from './policy';
+export default {
+  namespaced: true,
+  modules: {
+    dict,
+    policy,
+  },
+};

+ 48 - 0
admin-frame/lib/style/drawer.less

@@ -0,0 +1,48 @@
+.tabs-drawer {
+  /deep/ .el-drawer__header {
+    margin-bottom: 0;
+    padding: 10px;
+  }
+  /deep/ .el-drawer__body {
+    margin-top: -45px;
+    // margin-bottom: 45px;
+    padding: 5px;
+    height: 100%;
+    overflow: hidden; // 解决火狐下布局问题
+  }
+  /deep/ .el-drawer__close-btn {
+    z-index: 9999;
+  }
+  .el-tabs {
+    width: 100%;
+    height: 100%;
+  }
+}
+.el-dialog.no-header, .el-dialog__wrapper.no-header {
+  /deep/ .el-dialog {
+    margin-top: 10vh !important;
+  }
+  /deep/ .el-dialog__header {
+    padding: 0;
+  }
+  /deep/ .el-dialog__title {
+    display: none;
+  }
+  /deep/ .el-dialog__body {
+    min-height: 200px;
+    // max-height: 600px;
+    overflow: hidden;
+    display: flex;
+    > * {
+      flex: 1;
+    }
+  }
+  .el-tabs {
+    margin-top: -20px;
+  }
+  /deep/ .el-dialog__headerbtn {
+    top: 10px;
+    right: 10px;
+    z-index: 100;
+  }
+}

+ 68 - 0
admin-frame/lib/style/index.less

@@ -0,0 +1,68 @@
+html, body, #app{
+  width: 100%;
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  background: #fff;
+}
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+[class*=" el-icon-naf"], [class^=el-icon-naf] {
+  font-family: naf-icons!important;
+}
+::-webkit-scrollbar {/*滚动条整体样式*/
+  width: 8px;     /*高宽分别对应横竖滚动条的尺寸*/
+  height: 8px;
+}
+::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
+  box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
+  border-radius: 5px;
+  background: hsla(220,4%,58%,.3)
+}
+::-webkit-scrollbar-track {/*滚动条里面轨道*/
+  box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
+  background: #EDEDED;
+}
+.el-menu-item [class^=naf-icon],.el-submenu [class^=naf-icon] {
+  vertical-align: middle;
+  margin-right: 5px;
+  width: 24px;
+  text-align: center;
+  font-size: 18px;
+}
+.flex.el-tabs {
+  display: flex;
+  flex-direction: column;
+  .el-tabs__content {
+    flex: 1;
+  }
+}
+.el-message {
+  z-index: 9999 !important;
+}
+.el-transfer.compact {
+  .el-transfer-panel {
+    width: 160px;
+  }
+  .el-transfer__buttons {
+    padding: 0 10px;
+  }
+  .el-transfer__buttons > .el-transfer__button {
+    padding: 9px 5px;
+  }
+  .el-transfer__button + .el-transfer__button {
+    margin-left: 5px;
+  }
+  .el-transfer-panel__item {
+    width: 100%;
+  }
+}
+.el-tooltip__popper.is-dark {
+  opacity: 0.8;
+}
+.large-icon {
+  font-size: 2em;
+}

+ 56 - 0
admin-frame/lib/style/lite.less

@@ -0,0 +1,56 @@
+.lite {
+  //min-height: 100%;
+  min-width: 720px;
+  min-height: 500px;
+  height: 100%;
+  width: 100%;
+  padding-right: 10px;
+  // display: flex;
+  // justify-content: flex-start;
+  // flex-direction: row;
+  .el-card {
+    min-height: 100%;
+    margin-bottom: 10px;
+    display: flex;
+    flex-direction: column;
+  }
+  .el-card /deep/ .el-card__header {
+    padding: 10px;
+    .title {
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+    .action {
+      padding: 0px 10px;
+      float: right;
+      .el-button--text {
+        padding: 0px;   
+      }
+      .el-button + .el-button {
+        margin-left: 5px;   
+      }
+    }
+  }
+  .el-card /deep/ .el-card__body {
+    flex: 1;
+    padding: 0;
+  }
+  .el-card.details /deep/ .el-card__body {
+    padding: 10px;
+  }
+}
+
+.right.list /deep/ .el-card__body {
+  padding: 0;
+  /deep/ .el-table__body-wrapper {
+    min-height: 200px;
+  }
+}
+.el-table--mini .naf-icons {
+  font-size: 12px;
+}
+
+.el-table--mini .icon + .icon {
+  margin-left: 3px;
+}

+ 50 - 0
admin-frame/lib/style/mixed.less

@@ -0,0 +1,50 @@
+.mixed {
+  //min-height: 100%;
+  min-width: 800px;
+  min-height: 500px;
+  height: 100%;
+  width: 100%;
+  padding-right: 10px;
+  display: flex;
+  justify-content: flex-start;
+  flex-direction: row;
+}
+.el-card {
+  min-height: 100%;
+}
+.el-card /deep/ .el-card__header {
+  padding: 10px;
+}
+.left {
+  width: 260px;
+}
+.left .top {
+  justify-content: space-around;
+  align-items: left;
+}
+.right {
+  flex: 1;
+}
+.el-form {
+  max-width: 500px;
+}
+.el-card.left {
+  display: flex;
+  flex-direction: column;
+  /deep/ &>.el-card__body {
+    flex: 1;
+    display: flex;
+    // flex-direction: column;
+    padding: 0;
+    
+  /deep/ &>.el-scrollbar {
+    flex: 1;
+    /deep/ &>.el-scrollbar__wrap {
+      overflow-x: hidden;
+    }
+  }
+  /deep/ &>* {
+    flex: 1;
+    }
+  }
+}

+ 132 - 0
admin-frame/lib/utils/axios-wrapper.js

@@ -0,0 +1,132 @@
+/* eslint-disable require-atomic-updates */
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _ from 'lodash';
+import Axios from 'axios';
+import { Util, Error } from 'naf-core';
+import { Loading } from 'element-ui';
+import UserUtil 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);
+  }
+
+  $delete(uri, query, options = {}) {
+    return this.$request(uri, null, query, { ...options, method: 'delete' });
+  }
+
+  $post(uri, data = {}, query, options) {
+    return this.$request(uri, data, query, options);
+  }
+
+  async $request(uri, data, query, options) {
+    if (!uri) console.error('uri不能为空');
+    // TODO: 合并query和options
+    if (_.isObject(query) && _.isObject(options)) {
+      const params = query.params ? query.params : query;
+      options = { ...options, params };
+    } 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;
+    const loadingInstance = Loading.service({ lock: true, spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0)', customClass: 'large-icon' });
+
+    try {
+      const axios = Axios.create({
+        baseURL: this.baseUrl,
+      });
+      if (UserUtil.token) {
+        axios.defaults.headers.common.Authorization = UserUtil.token;
+      }
+      if (UserUtil.scope) {
+        axios.defaults.headers.common['x-domain'] = UserUtil.scope;
+      }
+      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 && options.unwrap != false) {
+        res = _.omit(res, ['errcode', '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, data = {} } = err.response;
+        console.log(err.response);
+        if (status === 401) errmsg = '用户认证失败,请重新登录';
+        if (status === 403) errmsg = '当前用户不允许执行该操作';
+        if (status === 400 && data.errcode) {
+          const { errcode, errmsg, details } = data;
+          console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+          return data;
+        }
+        if (data && data.error) {
+          const { status, error, message } = data;
+          console.warn(`[${uri}] fail: ${status}: ${error}-${message}`);
+          return { errcode: status || ErrorCode.SERVICE_FAULT, errmsg: error, details: message };
+        }
+      }
+      console.error(`[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - ${err.message}`);
+      return { errcode: ErrorCode.SERVICE_FAULT, errmsg, details: err.message };
+    } finally {
+      currentRequests -= 1;
+      if (currentRequests <= 0) {
+        currentRequests = 0;
+        loadingInstance.close();
+      }
+    }
+  }
+}

+ 18 - 0
admin-frame/lib/utils/filters.js

@@ -0,0 +1,18 @@
+/* eslint-disable func-names */
+/* eslint-disable no-param-reassign */
+import _ from 'lodash';
+import moment from 'moment';
+
+export function dict(value, codes) {
+  if (!value) return '';
+  value = value.toString();
+  if (codes) {
+    value = _.get(codes, [value]) || value;
+  }
+  return value;
+}
+
+export function date(value, formmat) {
+  if (!value) return '';
+  return moment(value).format(formmat);
+}

+ 124 - 0
admin-frame/lib/utils/store.js

@@ -0,0 +1,124 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+
+Vue.use(Vuex);
+
+const files = require.context('@/store', true, /^\.\/(?!-)[^.]+\.(js|mjs)$/);
+const filenames = files.keys();
+
+// Store
+let storeData = {};
+
+// Check if {dir.store}/index.js exists
+const indexFilename = filenames.find(filename => filename.includes('./index.'));
+
+if (indexFilename) {
+  storeData = getModule(indexFilename);
+}
+
+// If store is not an exported method = modules store
+if (typeof storeData !== 'function') {
+  // Store modules
+  if (!storeData.modules) {
+    storeData.modules = {};
+  }
+
+  for (const filename of filenames) {
+    let name = filename.replace(/^\.\//, '').replace(/\.(js|mjs)$/, '');
+    if (name === 'index') continue;
+
+    const namePath = name.split(/\//);
+
+    name = namePath[namePath.length - 1];
+    if (['state', 'getters', 'actions', 'mutations'].includes(name)) {
+      const module = getModuleNamespace(storeData, namePath, true);
+      appendModule(module, filename, name);
+      continue;
+    }
+
+    // If file is foo/index.js, it should be saved as foo
+    const isIndex = name === 'index';
+    if (isIndex) {
+      namePath.pop();
+    }
+
+    const module = getModuleNamespace(storeData, namePath);
+    const fileModule = getModule(filename);
+
+    name = namePath.pop();
+    module[name] = module[name] || {};
+
+    // if file is foo.js, existing properties take priority
+    // because it's the least specific case
+    if (!isIndex) {
+      module[name] = Object.assign({}, fileModule, module[name]);
+      module[name].namespaced = true;
+      continue;
+    }
+
+    // if file is foo/index.js we want to overwrite properties from foo.js
+    // but not from appended mods like foo/actions.js
+    const appendedMods = {};
+    if (module[name].appends) {
+      appendedMods.appends = module[name].appends;
+      for (const append of module[name].appends) {
+        appendedMods[append] = module[name][append];
+      }
+    }
+
+    module[name] = Object.assign({}, module[name], fileModule, appendedMods);
+    module[name].namespaced = true;
+  }
+}
+
+// createStore
+export const createStore =
+  storeData instanceof Function
+    ? storeData
+    : () => {
+        return new Vuex.Store(
+          Object.assign(
+            {
+              strict: process.env.NODE_ENV !== 'production',
+            },
+            storeData,
+            {
+              state: storeData.state instanceof Function ? storeData.state() : {},
+            }
+          )
+        );
+      };
+
+// Dynamically require module
+function getModule(filename) {
+  const file = files(filename);
+  const module = file.default || file;
+  if (module.commit) {
+    throw new Error('[nuxt] store/' + filename.replace('./', '') + ' should export a method which returns a Vuex instance.');
+  }
+  if (module.state && typeof module.state !== 'function') {
+    throw new Error('[nuxt] state should be a function in store/' + filename.replace('./', ''));
+  }
+  return module;
+}
+
+function getModuleNamespace(storeData, namePath, forAppend = false) {
+  if (namePath.length === 1) {
+    if (forAppend) {
+      return storeData;
+    }
+    return storeData.modules;
+  }
+  const namespace = namePath.shift();
+  storeData.modules[namespace] = storeData.modules[namespace] || {};
+  storeData.modules[namespace].namespaced = true;
+  storeData.modules[namespace].modules = storeData.modules[namespace].modules || {};
+  return getModuleNamespace(storeData.modules[namespace], namePath, forAppend);
+}
+
+function appendModule(module, filename, name) {
+  const file = files(filename);
+  module.appends = module.appends || [];
+  module.appends.push(name);
+  module[name] = file.default || file;
+}

+ 77 - 0
admin-frame/lib/utils/user-util.js

@@ -0,0 +1,77 @@
+/* eslint-disable no-console */
+
+export default {
+  get user() {
+    const val = sessionStorage.getItem('user');
+    if (!val || val == '') return null;
+    try {
+      if (val) return JSON.parse(val);
+    } catch (err) {
+      console.error(err);
+    }
+    return null;
+  },
+  set user(userinfo) {
+    if (!userinfo) {
+      sessionStorage.removeItem('user');
+    } else {
+      sessionStorage.setItem('user', JSON.stringify(userinfo));
+    }
+    if (this.unit) {
+      this.lastUnit = this.unit;
+    }
+  },
+  get token() {
+    return sessionStorage.getItem('token') || '';
+  },
+  get scope() {
+    return sessionStorage.getItem('x-domain') || '';
+  },
+  set token(token) {
+    if (!token) {
+      sessionStorage.removeItem('token');
+    } else {
+      sessionStorage.setItem('token', token);
+    }
+  },
+  get isGuest() {
+    return !this.user || this.user.role === 'guest';
+  },
+  get unit() {
+    if (!this.user || this.user.iss !== 'platform') return undefined;
+    const unit = this.user.sub.split('@', 2)[1] || 'master';
+    return unit;
+  },
+  get platform() {
+    const unit = this.unit || this.lastUnit;
+    return unit === 'master' ? 'master' : 'school';
+  },
+  set lastUnit(value) {
+    localStorage.setItem('unit', value);
+  },
+  get lastUnit() {
+    return localStorage.getItem('unit');
+  },
+  get roles() {
+    const val = sessionStorage.getItem('roles');
+    if (!val) return [];
+    try {
+      if (val) return JSON.parse(val);
+    } catch (err) {
+      console.error(err);
+    }
+    return [];
+  },
+  set roles(roles) {
+    if (!roles) {
+      sessionStorage.removeItem('roles');
+    } else {
+      sessionStorage.setItem('roles', roles && JSON.stringify(roles));
+    }
+  },
+  save({ userinfo, token, roles }) {
+    this.user = userinfo;
+    this.token = token;
+    this.roles = roles || [];
+  },
+};

+ 25 - 0
admin-frame/mock/menu.54/app.js

@@ -0,0 +1,25 @@
+module.exports = [
+  {
+    title: '应用管理',
+    path: '/xms',
+    icon: 'column',
+    module: '@app',
+    children: [
+      {
+        title: '应用信息',
+        path: '/xms/app',
+        icon: 'bill',
+      },
+      {
+        title: '资源管理',
+        path: '/xms/resources',
+        icon: 'tags',
+      },
+      {
+        title: '角色管理',
+        path: '/xms/roles',
+        icon: 'tag',
+      },
+    ],
+  },
+];

+ 20 - 0
admin-frame/mock/menu.54/authorize.js

@@ -0,0 +1,20 @@
+module.exports = [
+  {
+    title: '授权管理',
+    path: '/xms',
+    icon: 'auth',
+    module: '@authorize',
+    children: [
+      {
+        title: '机构授权',
+        path: '/xms/authorize/org',
+        icon: 'dept',
+      },
+      {
+        title: '群组授权',
+        path: '/xms/authorize/group',
+        icon: 'users',
+      },
+    ],
+  },
+];

+ 36 - 0
admin-frame/mock/menu.54/gaf.js

@@ -0,0 +1,36 @@
+module.exports = [
+  {
+    title: '系统管理',
+    path: '/gaf',
+    icon: 'system',
+    module: '@gaf',
+    children: [
+      {
+        title: '系统用户',
+        path: '/gaf/user',
+        icon: 'account',
+      },
+      {
+        title: '用户部门',
+        path: '/gaf/dept',
+        icon: 'dept',
+      },
+      {
+        title: '数据字典',
+        path: '/gaf/dict',
+        icon: 'dict',
+        visible: 'false',
+      },
+      {
+        title: '菜单管理',
+        path: '/gaf/menu',
+        icon: 'menu',
+      },
+      {
+        title: '日志审计',
+        path: '/gaf/log',
+        icon: 'log',
+      },
+    ],
+  },
+];

+ 86 - 0
admin-frame/mock/menu.54/index.js

@@ -0,0 +1,86 @@
+const { Router } = require('express');
+const gaf = require('./gaf');
+const user = require('./user');
+const app = require('./app');
+const authorize = require('./authorize');
+
+const link = require('./link');
+
+const router = Router();
+
+const navDatas = [
+  {
+    title: '用户管理',
+    path: '/xms',
+    module: '@user',
+  },
+  {
+    title: '授权管理',
+    path: '/xms',
+    module: '@authorize',
+  },
+  {
+    title: '应用管理',
+    path: '/xms',
+    module: '@app',
+  },
+  {
+    title: '系统管理',
+    path: '/gaf',
+    module: '@gaf',
+  },
+  // ...link,
+];
+
+const transItems = (items, pid) =>
+  items.map((p, i) => {
+    p.pid = pid || '0';
+    p.id = p.id || (pid ? `${pid}-${i + 1}` : `${i + 1}`);
+    if (p.children && p.children.length > 0) p.children = transItems(p.children, p.id);
+    return p;
+  });
+const datas = transItems([...user, ...authorize, ...app, ...gaf]);
+
+const MapMenu = (catalog = [], module = undefined) => item => ({
+  title: item.title,
+  options: {
+    icon: item.icon,
+    path: `/${item.module || module}${item.path}`,
+    url: item.url,
+    target: item.target,
+    tooltip: item.tooltip,
+    module: item.module,
+    platform: item.platform,
+    roles: item.roels,
+    tags: item.tags,
+    meta: { catalog: catalog.concat(item.title) },
+  },
+  children: (item.children || []).map(MapMenu(catalog.concat(item.title), item.module || module)),
+});
+
+const menus = datas.map(MapMenu());
+const modules = navDatas.map(MapMenu());
+
+/* GET menus define. */
+router.get('/menu/load', function(req, res, next) {
+  console.log("!!!I'm menu/load!!!");
+  res.json({ errcode: 0, errmsg: 'ok', data: { items: menus, modules } });
+});
+// router.get('/menu/:module', function(req, res, next) {
+//   const module = req.params.module;
+//   const items = menus.filter(p => p.options.module == module);
+//   res.json({ errcode: 0, errmsg: 'ok', data: { items, modules } });
+// });
+router.get('/menu/items', function(req, res, next) {
+  res.json({ errcode: 0, errmsg: 'ok', data: datas });
+});
+router.get('/menu/menus', function(req, res, next) {
+  res.json({ errcode: 0, errmsg: 'ok', data: navDatas });
+});
+router.get('/menu/:module/items', function(req, res, next) {
+  const module = req.params.module;
+  const items = datas.filter(p => p.options.module == module);
+  res.json({ errcode: 0, errmsg: 'ok', data: items });
+});
+
+module.exports = router;

+ 15 - 0
admin-frame/mock/menu.54/link.js

@@ -0,0 +1,15 @@
+module.exports = [
+  {
+    title: '友情链接',
+    icon: 'menu',
+    module: '@links',
+    children: [
+      {
+        title: '吉大正元',
+        url: 'http://www.jit.com.cn',
+        target: '_blank',
+        icon: 'lianjie',
+      },
+    ],
+  },
+];

+ 37 - 0
admin-frame/mock/menu.54/user.js

@@ -0,0 +1,37 @@
+module.exports = [
+  {
+    id: 'user',
+    title: '用户管理',
+    path: '/xms',
+    icon: 'account',
+    module: '@user',
+    children: [
+      {
+        id: 'user-info',
+        title: '用户信息',
+        path: '/xms/user',
+        icon: 'account',
+      },
+      {
+        title: '机构管理',
+        path: '/xms/org',
+        icon: 'dept',
+      },
+      {
+        title: '群组管理',
+        path: '/xms/group',
+        icon: 'users',
+      },
+      {
+        title: '注册审核',
+        path: '/xms/register',
+        icon: 'audit',
+      },
+      {
+        title: '黑名单管理',
+        path: '/xms/blacklist',
+        icon: 'user1',
+      },
+    ],
+  },
+];

+ 25 - 0
admin-frame/mock/menu/app.js

@@ -0,0 +1,25 @@
+module.exports = [
+  {
+    title: '应用管理',
+    path: '',
+    icon: 'column',
+    module: '@app',
+    children: [
+      {
+        title: '应用信息',
+        path: '/xms/app',
+        icon: 'bill',
+      },
+      {
+        title: '资源管理',
+        path: '/xms/resources',
+        icon: 'tags',
+      },
+      {
+        title: '角色管理',
+        path: '/xms/roles',
+        icon: 'tag',
+      },
+    ],
+  },
+];

+ 25 - 0
admin-frame/mock/menu/authorize.js

@@ -0,0 +1,25 @@
+module.exports = [
+  {
+    title: '授权管理',
+    path: '',
+    icon: 'auth',
+    module: '@auth',
+    children: [
+      {
+        title: '机构授权',
+        path: '/xms/auth/org',
+        icon: 'dept',
+      },
+      {
+        title: '群组授权',
+        path: '/xms/auth/group',
+        icon: 'users',
+      },
+      {
+        title: '规则授权',
+        path: '/xms/auth/rule',
+        icon: 'users',
+      },
+    ],
+  },
+];

+ 50 - 0
admin-frame/mock/menu/cred.js

@@ -0,0 +1,50 @@
+module.exports = [
+  {
+    title: '凭证管理',
+    path: '/xms/cred',
+    icon: 'cert',
+    module: '@cred',
+    children: [
+      {
+        title: '指纹信息',
+        path: '/xms/cred/fingerprint',
+        icon: 'tags',
+      },
+      {
+        title: '指静脉信息',
+        path: '/xms/cred/fingervein',
+        icon: 'tags',
+      },
+      {
+        title: '证书管理',
+        path: '/xms/cred/ukey',
+        icon: 'tags',
+      },
+      {
+        title: '动态令牌',
+        path: '/xms/cred/token',
+        icon: 'tags',
+      },
+      // {
+      //   title: '人脸',
+      //   path: '/xms/cred/face',
+      //   icon: 'tags',
+      // },
+      // {
+      //   title: '虹膜',
+      //   path: '/xms/cred/iris',
+      //   icon: 'tags',
+      // },
+      // {
+      //   title: 'TF卡',
+      //   path: '/xms/cred/tf',
+      //   icon: 'tags',
+      // },
+      // {
+      //   title: '用户身份卡',
+      //   path: '/xms/cred/identity',
+      //   icon: 'tag',
+      // },
+    ],
+  },
+];

+ 30 - 0
admin-frame/mock/menu/dev.js

@@ -0,0 +1,30 @@
+module.exports = [
+  {
+    title: '凭证设备',
+    path: '/xms/dev',
+    icon: 'mobile',
+    module: '@dev',
+    children: [
+      {
+        title: '证书UKey',
+        path: '/xms/dev/ukey',
+        icon: 'tag',
+      },
+      {
+        title: '动态令牌',
+        path: '/xms/dev/token',
+        icon: 'tag',
+      },
+      {
+        title: '用户身份卡',
+        path: '/xms/dev/identity',
+        icon: 'tag',
+      },
+      {
+        title: '证书TF卡',
+        path: '/xms/dev/tf',
+        icon: 'tag',
+      },
+    ],
+  },
+];

+ 50 - 0
admin-frame/mock/menu/gaf.js

@@ -0,0 +1,50 @@
+module.exports = [
+  {
+    title: '系统管理',
+    path: '',
+    icon: 'system',
+    module: '@gaf',
+    children: [
+      {
+        title: '系统用户',
+        path: '/gaf/user',
+        icon: 'account',
+      },
+      {
+        title: '用户部门',
+        path: '/gaf/dept',
+        icon: 'dept',
+      },
+      // {
+      //   title: '角色管理',
+      //   path: '/gaf/role',
+      //   icon: 'tag',
+      // },
+      {
+        title: '数据字典',
+        path: '/gaf/dict',
+        icon: 'dict',
+      },
+      {
+        title: '管理日志',
+        path: '/gaf/log',
+        icon: 'log',
+      },
+      {
+        title: '任务管理',
+        path: '/xms/task',
+        icon: 'log',
+      },
+      {
+        title: '用户同步管理',
+        path: '/xms/synchro',
+        icon: 'log',
+      },
+      {
+        title: '备份恢复',
+        path: '/xms/backups',
+        icon: 'log',
+      },
+    ],
+  },
+];

+ 140 - 0
admin-frame/mock/menu/index.js

@@ -0,0 +1,140 @@
+const { Router } = require('express');
+const gaf = require('./gaf');
+const user = require('./user');
+const log = require('./log');
+const app = require('./app');
+const authorize = require('./authorize');
+const policy = require('./policy');
+const link = require('./link');
+const register = require('./register');
+const cred = require('./cred');
+const journal = require('./journal');
+const dev = require('./dev');
+const terminal = require('./terminal');
+const router = Router();
+
+const navDatas = [
+  {
+    title: '用户管理',
+    path: '/xms',
+    module: '@user',
+  },
+  {
+    title: '终端管理',
+    path: '/xms',
+    module: '@terminal',
+  },
+  {
+    title: '用户审核',
+    path: '/xms/',
+    module: '@register',
+  },
+  {
+    title: '授权管理',
+    path: '/xms',
+    module: '@auth',
+  },
+  {
+    title: '凭证管理',
+    path: '/xms',
+    module: '@cred',
+  },
+  {
+    title: '凭证设备',
+    path: '/xms',
+    module: '@dev',
+  },
+  {
+    title: '应用管理',
+    path: '/xms',
+    module: '@app',
+  },
+  {
+    title: '策略管理',
+    path: '/xms',
+    module: '@policy',
+  },
+  {
+    title: '用户审计',
+    path: '/log/audit/logon',
+    module: '@log',
+  },
+  {
+    title: '责任认定',
+    path: '/log/audit/duty',
+    module: '@log',
+  },
+  {
+    title: '日志审计',
+    path: '/log/query/c1',
+    module: '@log',
+  },
+  {
+    title: '系统管理',
+    path: '/gaf',
+    module: '@gaf',
+  },
+  // ...link,
+];
+
+const home = [
+  {
+    title: '工作台',
+    path: '/xms/dashboard',
+    icon: 'home',
+    module: '@user',
+  },
+];
+
+const transItems = (items, pid) =>
+  items.map((p, i) => {
+    p.pid = pid || '0';
+    p.id = p.id || (pid ? `${pid}-${i + 1}` : `${i + 1}`);
+    if (p.children && p.children.length > 0) p.children = transItems(p.children, p.id);
+    return p;
+  });
+const datas = transItems([...home, ...user, ...terminal, ...register, ...authorize, ...cred, ...dev, ...app, ...policy, ...log, ...journal, ...gaf]);
+
+const MapMenu = (catalog = [], module = undefined) => item => ({
+  title: item.title,
+  options: {
+    icon: item.icon,
+    path: `/${item.module || module}${item.path}`,
+    url: item.url,
+    target: item.target,
+    tooltip: item.tooltip,
+    module: item.module,
+    platform: item.platform,
+    roles: item.roels,
+    tags: item.tags,
+    meta: { catalog: catalog.concat(item.title) },
+  },
+  children: (item.children || []).map(MapMenu(catalog.concat(item.title), item.module || module)),
+});
+
+const menus = datas.map(MapMenu());
+const modules = navDatas.map(MapMenu());
+
+/* GET menus define. */
+router.get('/menu/load', function(req, res, next) {
+  console.log("!!!I'm menu/load!!!");
+  res.json({ errcode: 0, errmsg: 'ok', data: { items: menus, modules } });
+});
+// router.get('/menu/:module', function(req, res, next) {
+//   const module = req.params.module;
+//   const items = menus.filter(p => p.options.module == module);
+//   res.json({ errcode: 0, errmsg: 'ok', data: { items, modules } });
+// });
+router.get('/menu/items', function(req, res, next) {
+  res.json({ errcode: 0, errmsg: 'ok', data: datas });
+});
+router.get('/menu/menus', function(req, res, next) {
+  res.json({ errcode: 0, errmsg: 'ok', data: navDatas });
+});
+router.get('/menu/:module/items', function(req, res, next) {
+  const module = req.params.module;
+  const items = datas.filter(p => p.options.module == module);
+  res.json({ errcode: 0, errmsg: 'ok', data: items });
+});
+
+module.exports = router;

+ 45 - 0
admin-frame/mock/menu/journal.js

@@ -0,0 +1,45 @@
+module.exports = [
+  {
+    title: '日志审计',
+    path: '',
+    icon: 'log',
+    module: '@log',
+    children: [
+      // {
+      //   title: '启动停止日志',
+      //   path: '/log/journal/c1',
+      //   icon: 'bill',
+      // },
+      {
+        title: '登录认证日志',
+        path: '/log/query/c2',
+        icon: 'bill',
+      },
+      {
+        title: '操作使用日志',
+        path: '/log/query/c3',
+        icon: 'bill',
+      },
+      {
+        title: '业务错误日志',
+        path: '/log/query/c4',
+        icon: 'bill',
+      },
+      {
+        title: '管理日志',
+        path: '/log/query/c5',
+        icon: 'bill',
+      },
+      {
+        title: '用户访问日志',
+        path: '/log/query/c6',
+        icon: 'bill',
+      },
+      {
+        title: '设备认证日志',
+        path: '/log/query/c7',
+        icon: 'bill',
+      },
+    ],
+  },
+];

+ 15 - 0
admin-frame/mock/menu/link.js

@@ -0,0 +1,15 @@
+module.exports = [
+  {
+    title: '友情链接',
+    icon: 'menu',
+    module: '@links',
+    children: [
+      {
+        title: '吉大正元',
+        url: 'http://www.jit.com.cn',
+        target: '_blank',
+        icon: 'lianjie',
+      },
+    ],
+  },
+];

+ 53 - 0
admin-frame/mock/menu/log.js

@@ -0,0 +1,53 @@
+module.exports = [
+  {
+    title: '用户审计',
+    path: '',
+    icon: 'report',
+    module: '@log',
+    children: [
+      {
+        title: '用户登录退出时间时段分布',
+        path: '/log/audit/logon/time',
+        icon: 'account',
+      },
+      {
+        title: '用户来源地域分布',
+        path: '/log/audit/logon/region',
+        icon: 'account',
+      },
+      {
+        title: '指定用户常用登录方式分布',
+        path: '/log/audit/logon/type',
+        icon: 'account',
+      },
+      {
+        title: '应用访问量时段分布',
+        path: '/log/audit/app/time',
+        icon: 'account',
+      },
+      {
+        title: '应用页面访问时长分布',
+        path: '/log/audit/app/page',
+        icon: 'account',
+      },
+      {
+        title: '应用模块访问时长分布',
+        path: '/log/audit/app/module',
+        icon: 'account',
+      },
+    ],
+  },
+  {
+    title: '责任认定',
+    path: '/log/audit/duty',
+    icon: 'auth3',
+    module: '@log',
+    children: [
+      {
+        title: '责任认定',
+        path: '/log/audit/duty',
+        icon: 'auth3',
+      },
+    ],
+  },
+];

+ 20 - 0
admin-frame/mock/menu/policy.js

@@ -0,0 +1,20 @@
+module.exports = [
+  {
+    title: '策略管理',
+    path: '',
+    icon: 'caogao',
+    module: '@policy',
+    children: [
+      {
+        title: '策略管理',
+        path: '/xms/policy',
+        icon: 'caogao',
+      },
+      {
+        title: '策略配置',
+        path: '/xms/policyValue',
+        icon: 'caogao',
+      },
+    ],
+  },
+];

+ 25 - 0
admin-frame/mock/menu/register.js

@@ -0,0 +1,25 @@
+module.exports = [
+  {
+    title: '用户审核',
+    path: '',
+    icon: 'idok',
+    module: '@user',
+    children: [
+      {
+        title: '待审核',
+        path: '/xms/register/pending',
+        icon: 'prompt',
+      },
+      {
+        title: '已审核',
+        path: '/xms/register/done',
+        icon: 'success',
+      },
+      {
+        title: '已驳回',
+        path: '/xms/register/rejected',
+        icon: 'error',
+      },
+    ],
+  },
+];

+ 14 - 0
admin-frame/mock/menu/terminal.js

@@ -0,0 +1,14 @@
+module.exports = [
+  {
+    title: '终端管理',
+    icon: 'menu',
+    module: '@terminal',
+    children: [
+      {
+        title: '终端管理',
+        path: '/xms/terminal',
+        icon: 'caogao',
+      },
+    ],
+  },
+];

+ 45 - 0
admin-frame/mock/menu/user.js

@@ -0,0 +1,45 @@
+module.exports = [
+  {
+    title: '用户管理',
+    path: '',
+    icon: 'idcard',
+    module: '@user',
+    children: [
+      {
+        title: '用户信息',
+        path: '/xms/user',
+        icon: 'idcard',
+      },
+      {
+        title: '机构管理',
+        path: '/xms/org',
+        icon: 'dept',
+      },
+      {
+        title: '群组管理',
+        path: '/xms/group',
+        icon: 'users',
+      },
+      {
+        title: '用户规则',
+        path: '/xms/rule',
+        icon: 'el-icon-s-order',
+      },
+      {
+        title: '帐号管理',
+        path: '/xms/acct',
+        icon: 'user',
+      },
+      {
+        title: '证书管理',
+        path: '/xms/cert',
+        icon: 'cert',
+      },
+      {
+        title: '黑名单管理',
+        path: '/xms/blacklist',
+        icon: 'user1',
+      },
+    ],
+  },
+];

+ 137 - 0
admin-frame/naf/chart/tendency.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="line1">
+    <div id="line1" class="" style="width: 90%;height:450px;"></div>
+  </div>
+</template>
+
+<script>
+import echarts from 'echarts/lib/echarts';
+// 引入柱状图
+import 'echarts/lib/chart/bar';
+import 'echarts/lib/chart/line';
+import 'echarts/lib/component/title';
+import 'echarts/lib/component/legend';
+import 'echarts/lib/component/toolbox';
+import 'echarts/lib/component/markPoint';
+import 'echarts/lib/component/tooltip';
+
+export default {
+  mounted() {
+    this.myChart = echarts.init(document.getElementById('line1'));
+    this.initData();
+  },
+  props: ['sevenDate', 'sevenDay'],
+  methods: {
+    initData() {
+      const colors = ['#5793f3', '#675bba', '#d14a61'];
+      const option = {
+        color: colors,
+        title: {
+          text: '走势图',
+          subtext: ''
+        },
+        tooltip: {
+          trigger: 'axis'
+        },
+        legend: {
+          data: ['新注册用户', '新增信息', '新增管理员']
+        },
+        toolbox: {
+          show: true,
+          feature: {
+            dataZoom: {
+              yAxisIndex: 'none'
+            },
+            dataView: { readOnly: false },
+            magicType: { type: ['bar', 'line'] },
+            restore: {}
+          }
+        },
+        xAxis: {
+          type: 'category',
+          boundaryGap: false,
+          data: this.sevenDay
+        },
+        yAxis: [
+          {
+            type: 'value',
+            name: '用户',
+            min: 0,
+            max: 200,
+            position: 'left',
+            axisLine: {
+              lineStyle: {
+                color: '#999'
+              }
+            },
+            axisLabel: {
+              formatter: '{value}'
+            }
+          },
+          {
+            type: 'value',
+            name: '信息',
+            min: 0,
+            max: 200,
+            position: 'right',
+            axisLine: {
+              lineStyle: {
+                color: '#999'
+              }
+            },
+            axisLabel: {
+              formatter: '{value}'
+            }
+          }
+        ],
+        series: [
+          {
+            name: '新注册用户',
+            type: 'line',
+            data: this.sevenDate[0],
+            yAxisIndex: 1,
+            markPoint: {
+              data: [{ type: 'max', name: '最大值' }, { type: 'min', name: '最小值' }]
+            }
+          },
+          {
+            name: '新增信息',
+            type: 'line',
+            data: this.sevenDate[1],
+            yAxisIndex: 1,
+            markPoint: {
+              data: [{ type: 'max', name: '最大值' }, { type: 'min', name: '最小值' }]
+            }
+          },
+          {
+            name: '新增管理员',
+            type: 'line',
+            data: this.sevenDate[2],
+            yAxisIndex: 1,
+            markPoint: {
+              data: [{ type: 'max', name: '最大值' }, { type: 'min', name: '最小值' }]
+            }
+          }
+        ]
+      };
+      this.myChart.setOption(option);
+    }
+  },
+  watch: {
+    sevenDate() {
+      this.initData();
+    },
+    sevenDay() {
+      this.initData();
+    }
+  }
+};
+</script>
+
+<style lang="less">
+@import '~@/style/mixin';
+.line1 {
+  display: flex;
+  justify-content: center;
+}
+</style>

+ 87 - 0
admin-frame/naf/data/README.md

@@ -0,0 +1,87 @@
+# 基于数据驱动的数据视图,具备增删改查基础功能
+## 组件说明
+### list
+弃用,用filter-grid代替
+### lite-grid
+简单数据列表,不包含查询和分页
+### filter-grid
+组合数据列表,包含查询和分页功能
+### meta-util
+数据定义处理工具包
+### 其他
+demo-table 演示数据列表
+naf-table 最早构建的代码框架,已无用
+
+## 字段定义
+### Meta定义形式1
+{
+  name: String, //字段名称
+  label: String, //显示名称
+  required: Boolean, //是否必须
+  readonly: Boolean, //字段是否只读,默认false
+  editable: Boolean, //字段是否支持编辑,默认true
+  slots: Array, // 显示区域,默认为['list', 'form'],可选值:'list'、'form'、'filter'
+  rules: Array, //表单校验规则
+  order: Number, // 显示顺序,默认为0,数字越大越靠前
+  listOpts: Object, //数据列表可选参数
+  formOpts: Object, //数据表单可选参数
+}
+### Meta定义形式2
+{
+  field: {
+    name: String, //字段名称
+    label: String, //显示名称
+    required: Boolean, //是否必须
+    readonly: Boolean, //字段是否只读,默认false
+    editable: Boolean, //字段是否支持编辑,默认true
+  },
+  slots: Array, // 显示区域,默认为['list', 'form'],可选值:'list'、'form'、'filter'
+  rules: Array, //表单校验规则
+  order: Number, // 显示顺序,默认为0,数字越大越靠前
+  listOpts: Object, //数据列表可选参数
+  formOpts: Object, //数据表单可选参数
+}
+### Meta定义形式3
+{
+  field: [name,label,required,readonly,editable,filter],
+  slots: Object, // 显示区域,示例:{ filter: false, list: true, form: true }
+  rules: Array, //表单校验规则
+  order: Number, // 显示顺序,默认为0,数字越大越靠前
+  listOpts: Object, //数据列表可选参数
+  formOpts: Object, //数据表单可选参数
+}
+### Meta定义形式4(预留,暂不支持)
+[
+  [name,label,required,readonly,editable,filter],
+  [slots: Array], // 显示区域,默认为['list', 'form'],可选值:'list'、'form'、'filter'
+  [rules: Array], //表单校验规则
+  [
+    order: Number, // 显示顺序,默认为0,数字越大越靠前
+    listOpts: Object, //数据列表可选参数
+    formOpts: Object, //数据表单可选参数
+  ]
+]
+
+## 列表操作定义
+### 定义形式1-简单对象
+{
+  edit: '编辑',
+  delete: '删除',
+  view: '查看',
+}
+
+### 定义形式2-简单数组
+[
+  [ 'edit', '编辑' ],
+  [ 'delete', '删除', true /*是否进行确认提示*/ ],
+  [ 'view', '查看' ],
+]
+
+### 定义形式3-数组对象(终极形态)
+[
+  { event: 'edit', label: '编辑' },
+  { event: 'delete', label: '删除', confirm: true },
+  { event: 'view', label: '查看' },
+]
+
+

+ 66 - 0
admin-frame/naf/data/code-select.vue

@@ -0,0 +1,66 @@
+<template>
+  <el-select v-model="selected" :placeholder="placeholder || '请选择'" @change="handleChange" :disabled="disabled">
+    <template v-for="(item, _index) in datas">
+      <el-option-group :key="'option-group-' + _index" :label="item.name" v-if="item.children && item.children.length > 0">
+        <el-option
+          v-for="(child, _index2) in item.children"
+          :key="'option-item-' + _index + '-' + _index2"
+          :label="child.name"
+          :value="child.code"
+          :disabled="child.status == '1'"
+        ></el-option>
+      </el-option-group>
+      <el-option :key="'option-item-' + _index" :label="item.name" :value="item.code" :disabled="item.status == '1'" v-else></el-option>
+    </template>
+  </el-select>
+</template>
+<script>
+import { createNamespacedHelpers } from 'vuex';
+const { mapActions } = createNamespacedHelpers('naf/dict');
+
+export default {
+  name: 'code-select',
+  props: {
+    value: { required: true },
+    codeType: { type: String, required: true },
+    placeholder: String,
+    disabled: Boolean,
+    mode: { type: String, default: 'code' }, // 选值模式:code、name、pair
+  },
+  data() {
+    return {
+      selected: this.value,
+      datas: [],
+    };
+  },
+  async mounted() {
+    const res = await this.load(this.codeType);
+    if (!res.errcode) {
+      this.datas = res.data || res;
+    } else {
+      // eslint-disable-next-line no-console
+      console.error(`数据字典[${this.codeType}]加载失败:`, res);
+    }
+  },
+  methods: {
+    ...mapActions(['load']),
+    handleChange() {
+      if (this.selected) {
+        const items = this.datas || [];
+        const item = items.find(p => p.code === this.selected);
+        if (item && this.mode === 'name') {
+          this.$emit('input', item.name);
+          return;
+        }
+        if (this.mode === 'pair') {
+          this.$emit('input', item);
+          return;
+        }
+      }
+
+      this.$emit('input', this.selected);
+      // this.$emit('change', this.selected);
+    },
+  },
+};
+</script>

+ 96 - 0
admin-frame/naf/data/demo-table.vue

@@ -0,0 +1,96 @@
+<template>
+  <el-container class="container">
+    <el-header height="36px" style="line-height:36px;">
+      <div class="filter-box">
+        <el-input placeholder="请输入内容" class="input-with-select" :clearable="true" size="mini" v-model="fieldValue">
+          <el-select slot="prepend" placeholder="请选择" width="110" v-model="fieldName">
+            <el-option v-for="(item, index) in filters" :label="item" :value="item" :key="'filter' + index"></el-option>
+          </el-select>
+          <el-button slot="append" icon="el-icon-search" @click="query"></el-button>
+        </el-input>
+      </div>
+      <el-button icon="el-icon-plus" type="primary" size="mini" v-if="showAction">添加</el-button>
+    </el-header>
+    <el-main>
+      <el-table border style="width: 100%;overflow: auto;" size="mini">
+        <el-table-column v-for="(item, index) in fields" :label="item" :key="'field' + index"></el-table-column>
+        <el-table-column label="操作" v-if="showAction"></el-table-column>
+      </el-table>
+    </el-main>
+    <el-footer height="36px">
+      <el-pagination
+        background
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="page"
+        :page-sizes="[10, 20, 50, 100, 200]"
+        :page-size="size"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+      ></el-pagination>
+    </el-footer>
+  </el-container>
+</template>
+<script>
+export default {
+  name: 'demo-table',
+  props: {
+    fields: Array,
+    filters: Array,
+    readonly: Boolean,
+  },
+  data() {
+    return {
+      loading: false,
+      dataSource: [],
+      total: 0,
+      page: 1,
+      size: 20,
+      fieldName: '',
+      fieldValue: '',
+      showAction: !this.readonly,
+    };
+  },
+  mounted() {
+    this.query();
+  },
+  methods: {
+    async query() {
+      this.loading = true;
+      setTimeout(() => {
+        this.loading = false;
+      }, 1000);
+    },
+    handleSizeChange(val) {
+      console.log(`每页 ${val} 条`);
+      this.size = val;
+      if (this.total === 0) return;
+      const pages = Math.floor((this.total + val) / val);
+      if (pages < this.page) this.page = pages;
+      this.query();
+    },
+    handleCurrentChange(val) {
+      console.log(`当前页: ${val}`);
+      this.page = val;
+      this.query();
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.container {
+  width: 100%;
+  height: 100%;
+}
+.el-table {
+  height: 100%;
+  overflow: auto;
+}
+.filter-box {
+  width: 280px;
+  display: inline-block;
+  .el-select {
+    width: 100px;
+  }
+}
+</style>

+ 145 - 0
admin-frame/naf/data/filter-box.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="filter-box" :class="{ multiple }">
+    <el-input v-if="!multiple" placeholder="请输入内容" class="input-with-select" :clearable="true" size="mini" v-model="filterData.value">
+      <el-select class="prepend" slot="prepend" placeholder="请选择" v-model="filterData.name" @clear="handleClear" clearable>
+        <el-option v-for="(item, index) in fields" :label="item.field.label" :value="item.field.name" :key="'filter' + index"></el-option>
+      </el-select>
+      <el-button slot="append" icon="el-icon-search" @click="query"></el-button>
+    </el-input>
+    <el-form v-else ref="form" :model="form" :inline="true">
+      <el-form-item v-for="(item, index) in simpleFields" :key="'form-field-' + index" :label="item.field.label">
+        <naf-input v-model="form[item.field.name]" size="mini" :dict="item.dict" :clearable="true"></naf-input>
+      </el-form-item>
+      <el-form-item>
+        <el-dropdown v-if="matcher == 'all'" size="mini" split-button type="primary" @click="query" @command="handleCommand">
+          查询
+          <el-dropdown-menu slot="dropdown">
+            <el-dropdown-item command="exact">精确查询<i v-if="matching == 'exact'" class="el-icon-check el-icon--right"></i></el-dropdown-item>
+            <el-dropdown-item command="like">模糊查询<i v-if="matching == 'like'" class="el-icon-check el-icon--right"></i></el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
+        <el-button v-else type="primary" size="mini" @click="query">查询</el-button>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" size="mini" @click="reset">重置</el-button>
+      </el-form-item>
+      <el-form-item v-if="$scopedSlots.ext || moreFields.length > 0">
+        <el-button type="primary" size="mini" @click="toggleMore" v-if="!showMore">更多</el-button>
+        <el-button type="default" size="mini" @click="toggleMore" v-else>收起</el-button>
+      </el-form-item>
+    </el-form>
+    <slot name="ext" v-bind="{ form, handleFieldChange }" v-if="showMore">
+      <el-form ref="form-ext" :model="form" :inline="true">
+        <el-form-item v-for="(item, index) in moreFields" :key="'form-ex-field-' + index" :label="item.field.label">
+          <naf-input v-model="form[item.field.name]" size="mini" :dict="item.dict" :clearable="true"></naf-input>
+        </el-form-item>
+      </el-form>
+    </slot>
+  </div>
+</template>
+
+<script>
+import NafInput from './naf-input';
+import { Util } from 'naf-core';
+
+const defaultMatcher = process.env.VUE_APP_FILTER_MATCHER_DEFAULT || 'like';
+
+export default {
+  components: {
+    NafInput,
+  },
+  props: {
+    fields: { type: Array, required: true },
+    multiple: { type: Boolean, default: eval(process.env.VUE_APP_FILTER_MULTI || false) },
+    matcher: { type: String, default: process.env.VUE_APP_FILTER_MATCHER || 'like' },
+    maxFields: { type: Number, default: 4 },
+  },
+  data() {
+    return {
+      filterData: { name: '', value: '' },
+      form: {},
+      matching: (defaultMatcher == 'exact' && 'exact') || (defaultMatcher == 'like' && 'like') || 'like',
+      showMore: false,
+    };
+  },
+  methods: {
+    async query() {
+      const filter = { ...this.filter, exact: this.matching != 'like' };
+      const paging = { page: this.page, size: this.size };
+      Object.freeze(filter);
+      Object.freeze(paging);
+      this.$emit('query', {
+        filter,
+        paging,
+      });
+    },
+    handleClear() {
+      this.filterData = { name: '', value: '' };
+    },
+    reset() {
+      this.form = {};
+      this.query();
+    },
+    handleCommand(command) {
+      this.matching = command;
+      this.query();
+    },
+    toggleMore() {
+      this.showMore = !this.showMore;
+    },
+    handleFieldChange(name, value) {
+      console.debug(`filter field ${name} changed:`, value);
+      this.$set(this.form, name, value);
+    },
+  },
+  computed: {
+    filterFields() {
+      return this.fields.map(p => ({
+        ...p,
+        dict: p.dict || (this.$dict && p.formatter && p.formatter.name === 'dict' && this.$dict(p.formatter.param)),
+      }));
+    },
+    filter() {
+      if (this.multiple) {
+        const entries = Object.entries(this.form).filter(p => p[1] != undefined && p[1] != null && p[1] != '');
+        return Object.fromEntries(entries);
+      }
+      let filter = { [this.filterData.name]: this.filterData.value };
+      if (this.filterData.name == undefined || this.filterData.value == undefined || this.filterData.value == '') {
+        filter = undefined;
+      }
+      return filter;
+    },
+    simpleFields() {
+      return this.filterFields.slice(0, this.maxFields);
+    },
+    moreFields() {
+      return this.filterFields.slice(this.maxFields) || [];
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.filter-box {
+  width: 300px;
+  display: inline-block;
+  margin-right: 20px;
+  .prepend.el-select {
+    width: 100px;
+  }
+}
+.filter-box.multiple {
+  width: 100%;
+  min-width: 920px;
+  .naf-input {
+    width: 100px;
+    /deep/ .el-input__inner {
+      padding: 0 5px;
+    }
+  }
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 189 - 0
admin-frame/naf/data/filter-grid.vue

@@ -0,0 +1,189 @@
+<template>
+  <el-container class="container">
+    <el-header height="auto" style="line-height:36px;" v-if="filter || action">
+      <slot name="filter" v-if="filter">
+        <filter-box ref="filter" :matcher="matcher" :fields="filterFields" @query="query" v-bind="filterProps">
+          <template v-slot:ext="form" v-if="$scopedSlots['filter-ext']">
+            <slot name="filter-ext" v-bind="form"></slot>
+          </template>
+        </filter-box>
+      </slot>
+      <slot name="action" v-if="action">
+        <el-button icon="el-icon-plus" type="primary" size="mini" v-if="!readonly" @click="$emit('add-new')">添加</el-button>
+      </slot>
+    </el-header>
+    <el-main class="table-area">
+      <lite-grid
+        :data="data"
+        :meta="meta"
+        :selection="selection"
+        :options="options"
+        :readonly="readonly"
+        :operation="operation"
+        :currentRow="currentRow"
+        @oper="handleOper"
+        @selection="$emit('selection', $event)"
+        @dblclick="$emit('dblclick', $event)"
+        @currentRow="$emit('row', $event)"
+      >
+        <template v-slot:pre>
+          <slot name="pre"> </slot>
+        </template>
+        <slot> </slot>
+        <template v-slot:oper>
+          <slot name="oper"> </slot>
+        </template>
+        <template v-slot:oper_header>
+          <slot name="oper_header"> </slot>
+        </template>
+        <template v-slot:ext>
+          <slot name="ext"> </slot>
+        </template>
+        <template v-slot:post>
+          <slot name="post"> </slot>
+        </template>
+      </lite-grid>
+    </el-main>
+    <el-footer height="36px" v-if="paging">
+      <el-pagination
+        background
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="page"
+        :page-sizes="[10, 20, 50, 100, 200]"
+        :page-size="size"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+      ></el-pagination>
+    </el-footer>
+  </el-container>
+</template>
+<script>
+import { FieldMeta } from './meta-util';
+import LiteGrid from './lite-grid';
+import FilterBox from './filter-box';
+
+export default {
+  name: 'filter-grid',
+  components: {
+    LiteGrid,
+    FilterBox,
+  },
+  props: {
+    meta: { type: Array, required: true },
+    readonly: Boolean /* 是否显示操作列 */,
+    filter: { type: Boolean, default: false } /* 是否显示查询 */,
+    action: { type: Boolean, default: false } /* 是否显操作按钮 */,
+    paging: { type: Boolean, default: false } /* 是否显示分页 */,
+    selection: { type: Boolean, default: false } /* 是否显示多选 */,
+    currentRow: { type: Boolean, default: false }, // 是否显示单行高亮
+    options: {
+      type: Object,
+      default: () => ({ size: 'mini' }),
+    } /* 表格扩展属性 */,
+    operation: Array,
+    data: Array,
+    total: { type: Number, default: 0 } /* 总数据条数 */,
+    pageSize: { type: Number, default: 10 },
+    filterProps: { type: Object } /* filterBox属性 */,
+    matcher: { type: String },
+  },
+  data() {
+    return {
+      page: 1,
+      size: this.pageSize,
+      currentFilter: {},
+      // selectionList: [],
+      disableQuery: false,
+    };
+  },
+  methods: {
+    async query({ filter } = {}) {
+      this.currentFilter = filter;
+      this.$emit('query', {
+        filter,
+        paging: { page: this.page, size: this.size },
+      });
+    },
+    handleSizeChange(val) {
+      this.size = val;
+      if (this.total === 0) return;
+      if (this.page > this.pages) this.page = this.pages;
+      this.refresh();
+    },
+    handleCurrentChange(val) {
+      this.page = val;
+      // this.refresh(); 改为由watch触发refresh
+    },
+    async handleOper({ event, data }) {
+      this.$emit(event, data);
+    },
+    resetPage(page = 1) {
+      if (page == -1) {
+        this.page = this.pages; // 跳到尾页
+      } else {
+        this.page = 1;
+      }
+    },
+    refresh({ page } = {}) {
+      this.page = page || this.page;
+      return this.query({ filter: this.currentFilter });
+    },
+    reset() {
+      this.disableQuery = true;
+      this.resetPage();
+      this.$refs.filter.reset();
+      this.disableQuery = false;
+    },
+  },
+  computed: {
+    filterFields() {
+      return this.meta
+        .map(FieldMeta)
+        .filter(p => p.slots.filter)
+        .sort((a, b) => a.filterOpts.order - b.filterOpts.order || a.index - b.index);
+    },
+    pages() {
+      if (this.total == 0) return 1;
+      return Math.floor((this.total + this.size - 1) / this.size);
+    },
+  },
+  watch: {
+    total(val, old) {
+      if (val > 0 && val <= this.page * this.size) {
+        if (this.page > this.pages) {
+          this.page = this.pages;
+        }
+      } else if (val == 0 && this.page > 1) {
+        this.disableQuery = true;
+        this.resetPage();
+        this.disableQuery = false;
+      }
+    },
+    page(val, old) {
+      if (val != old && !this.disableQuery) {
+        this.refresh();
+      }
+    },
+    data() {
+      if (this.data && this.page < this.pages && this.data.length < this.size) {
+        this.refresh();
+      }
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.container {
+  width: 100%;
+  height: 100%;
+  max-height: 90vh;
+}
+.el-table {
+  height: 100%;
+  overflow: auto;
+}
+.el-main {
+  padding: 10px;
+}
+</style>

+ 102 - 0
admin-frame/naf/data/form-dlg.vue

@@ -0,0 +1,102 @@
+<template>
+  <el-dialog :title="title" :visible="visible" :width="width" :close-on-click-modal="false" @close="$emit('cancel')">
+    <el-form ref="form" :model="form" :rules="rules" v-bind="options">
+      <slot>
+        <el-form-item
+          v-for="(item, index) in fields"
+          :key="'form-field-' + index"
+          :label="item.field.label"
+          :prop="item.field.name"
+          :required="item.field.required"
+          :rules="item.rules"
+          v-bind="item.formOpts"
+        >
+          <naf-input
+            v-model="form[item.field.name]"
+            :disabled="readonly || item.field.readonly || (!isNew && item.field.editable === false)"
+            :placeholder="(item.formOpts && item.formOpts.placeholder) || (isNew && item.field.readonly && '--- 系统生成 ---')"
+            :dict="item.dict"
+            :type="item.formOpts && item.formOpts.inputType"
+            v-bind="mergeLength(item)"
+          ></naf-input>
+        </el-form-item>
+      </slot>
+    </el-form>
+    <div slot="footer" class="dialog-footer">
+      <el-button type="primary" @click="handleSave" :size="options.size" v-show="!readonly">确 定</el-button>
+      <el-button @click="$emit('cancel')" :size="options.size">取 消</el-button>
+    </div>
+  </el-dialog>
+</template>
+<script>
+import { FieldMeta } from './meta-util';
+import NafInput from './naf-input';
+
+export default {
+  name: 'data-dlg',
+  components: {
+    NafInput,
+  },
+  props: {
+    data: { type: Object, required: true },
+    meta: { type: Array, required: true },
+    rules: Object,
+    readonly: { type: Boolean, default: false } /* 是否只读 */,
+    isNew: { type: Boolean, default: false } /* 是否新创建 */,
+    options: {
+      type: Object,
+      default: () => ({ size: 'small' }),
+    } /* form options */,
+    width: String,
+    title: String,
+    visible: { type: Boolean, default: true },
+  },
+  data() {
+    return {
+      form: { ...this.data },
+    };
+  },
+  methods: {
+    handleSave() {
+      this.$refs['form'].validate(valid => {
+        if (valid) {
+          this.$emit('save', { isNew: this.isNew, data: this.form });
+        } else {
+          console.warn('form validate error!!!');
+        }
+      });
+    },
+    mergeRules(meta) {
+      if (
+        meta.field.required &&
+        (!this.rules || !this.rules[meta.field.name] || !this.rules[meta.field.name].some(p => p.required)) &&
+        (!meta.rules || !meta.rules || !meta.rules.some(p => p.required))
+      ) {
+        const rules = meta.rules || [];
+        rules.push({ required: true, message: '不能为空' });
+        return rules;
+      } else {
+        return meta.rules;
+      }
+    },
+    mergeLength(meta) {
+      const rules = [...((this.rules && this.rules[meta.field.name]) || []), ...(meta.rules || [])];
+      const { max, min } = rules.find(r => r.max || r.min) || {};
+      return { maxlength: max || 48, minlength: min || 0 };
+    },
+  },
+  computed: {
+    fields() {
+      return this.meta
+        .map(FieldMeta)
+        .filter(p => p.slots.form)
+        .sort((a, b) => a.order - b.order || a.index - b.index)
+        .map(p => ({
+          ...p,
+          dict: this.$dict && p.formatter && p.formatter.name === 'dict' && this.$dict(p.formatter.param),
+          rules: this.mergeRules(p),
+        }));
+    },
+  },
+};
+</script>

+ 143 - 0
admin-frame/naf/data/form.vue

@@ -0,0 +1,143 @@
+<template>
+  <el-form ref="form" :model="form" :rules="rules" v-bind="options">
+    <slot name="pre"></slot>
+    <slot>
+      <el-form-item
+        v-for="(item, index) in fields"
+        :key="'form-field-' + index"
+        :label="item.field.label"
+        :prop="item.field.name"
+        :required="item.field.required"
+        :rules="item.rules"
+        v-bind="item.formOpts"
+        v-show="isNew || !item.slots.nonce"
+      >
+        <slot name="field" v-if="item.slots.ext" v-bind="{ item, form, handleFieldChange, setFieldValue }">
+          <span>customEditor</span>
+        </slot>
+        <naf-input
+          v-else
+          v-model="form[item.field.name]"
+          :disabled="readonly || item.field.readonly || (!isNew && item.field.editable === false)"
+          :placeholder="(item.formOpts && item.formOpts.placeholder) || (isNew && item.field.readonly && '--- 系统生成 ---')"
+          :dict="item.dict"
+          :type="item.formOpts && item.formOpts.inputType"
+          v-bind="{ ...item.formOpts, ...mergeLength(item) }"
+          @change="handleFieldChange(item.field.name, $event)"
+        ></naf-input>
+      </el-form-item>
+    </slot>
+    <slot name="ext" v-bind:form="form"></slot>
+    <slot name="submit">
+      <el-form-item v-show="!readonly">
+        <el-button type="primary" @click="handleSave">{{ closeOnSaved ? '保存并关闭' : '保存' }}</el-button>
+        <el-button type="info" @click="$emit('cancel')">取消</el-button>
+      </el-form-item>
+    </slot>
+  </el-form>
+</template>
+<script>
+import { FieldMeta } from './meta-util';
+import NafInput from './naf-input';
+
+export default {
+  name: 'data-form',
+  components: {
+    NafInput,
+  },
+  props: {
+    data: { type: Object, required: true },
+    meta: { type: Array, required: true },
+    rules: Object,
+    readonly: { type: Boolean, default: false } /* 是否只读 */,
+    isNew: { type: Boolean, default: false } /* 是否新创建 */,
+    options: Object,
+    onlyModified: { type: Boolean, default: false } /** 是否只保存修改字段 */,
+    closeOnSaved: { type: Boolean, default: false } /** 保存并关闭 */,
+  },
+  data() {
+    return {
+      form: {},
+      modified: {},
+    };
+  },
+  mounted() {
+    this.reset();
+  },
+  methods: {
+    reset() {
+      this.form = { ...this.data };
+    },
+    handleSave() {
+      this.$refs['form'].validate(valid => {
+        if (valid) {
+          this.$emit('save', {
+            isNew: this.isNew,
+            isModified: this.isModified,
+            data: this.onlyModified ? this.modified : this.form,
+            closeOnSaved: this.closeOnSaved,
+          });
+        } else {
+          console.warn('form validate error!!!');
+        }
+      });
+    },
+    mergeRules(meta) {
+      const rules = meta.rules || [];
+      if (meta.pattern) {
+        const pattern = this.$policy(meta.pattern);
+        if (pattern !== 0) rules.push({ pattern, whitespace: true, message: '请输入正确内容' });
+      }
+      if (
+        meta.field.required &&
+        (!this.rules || !this.rules[meta.field.name] || !this.rules[meta.field.name].some(p => p.required)) &&
+        (!meta.rules || !meta.rules || !meta.rules.some(p => p.required))
+      ) {
+        rules.push({ required: true, whitespace: true, message: '不能为空' });
+        return rules;
+      } else {
+        return meta.rules;
+      }
+    },
+    mergeLength(meta) {
+      const rules = [...((this.rules && this.rules[meta.field.name]) || []), ...(meta.rules || [])];
+      const { max: maxlength, min: minlength } = rules.find(r => r.max || r.min) || {};
+      return { maxlength, minlength };
+    },
+    validateField(payload) {
+      this.$refs['form'].validateField(payload);
+    },
+    handleFieldChange(name, value) {
+      console.debug(`field ${name} changed:`, value);
+      if (this.data[name] === value) {
+        this.$delete(this.modified, name);
+      } else {
+        this.$set(this.modified, name, value);
+      }
+    },
+    setFieldValue(name, value) {
+      this.$set(this.form, name, value);
+      this.handleFieldChange(name, value);
+    },
+  },
+  computed: {
+    fields() {
+      return this.meta
+        .map(FieldMeta)
+        .filter(p => p.slots.form)
+        .sort((a, b) => a.order - b.order || a.index - b.index)
+        .map(p => ({
+          ...p,
+          dict: p.dict || (this.$dict && p.formatter && p.formatter.name === 'dict' && this.$dict(p.formatter.param)),
+          rules: this.mergeRules(p),
+        }));
+    },
+    isModified() {
+      return this.getModifiedFields.length > 0;
+    },
+    getModifiedFields() {
+      return Object.keys(this.modified);
+    },
+  },
+};
+</script>

+ 143 - 0
admin-frame/naf/data/lite-grid.vue

@@ -0,0 +1,143 @@
+<template>
+  <el-table
+    border
+    style="width: 100%;overflow: auto;"
+    :size="options.size || 'mini'"
+    v-bind="options"
+    :data="data"
+    :highlight-current-row="currentRow"
+    @selection-change="selectionChange"
+    @row-dblclick="dblclick"
+    @current-change="handleCurrentChange"
+  >
+    <el-table-column v-if="selection" type="selection" width="55"></el-table-column>
+    <slot name="pre"> </slot>
+    <slot>
+      <el-table-column
+        v-for="(item, index) in listFields"
+        :key="'field' + index"
+        :label="item.label"
+        :prop="item.name"
+        :formatter="item.formatter"
+        v-bind="item.options"
+        show-overflow-tooltip
+      />
+    </slot>
+    <slot name="ext"> </slot>
+    <slot name="oper">
+      <el-table-column label="操作" :width="options.operWidth || '100'" v-if="!readonly">
+        <template v-slot:header>
+          <slot name="oper_header">
+            <span>操作</span>
+          </slot>
+        </template>
+        <template v-slot="{ row, $index }">
+          <el-button
+            v-for="(item, index) in operItems"
+            :key="'field' + index"
+            @click="handleOper(item, row, $index, $event)"
+            type="text"
+            :size="options.size || 'mini'"
+            @focus="handleFocus($event)"
+          >
+            <el-tooltip v-if="item.icon" :content="item.label"><i :class="item.icon"></i></el-tooltip>
+            <span v-else>{{ item.label }}</span>
+          </el-button>
+        </template>
+      </el-table-column>
+    </slot>
+    <slot name="post"> </slot>
+  </el-table>
+</template>
+<script>
+import _ from 'lodash';
+import { FieldMeta, Operation, Formatter, MergeFilters } from './meta-util';
+
+export default {
+  name: 'lite-grid',
+  props: {
+    currentRow: Boolean, // 是否显示单行高亮
+    meta: { type: Array, required: true },
+    readonly: Boolean /* 是否显示操作列 */,
+    selection: Boolean /* 是否显示多选 */,
+    options: {
+      type: Object,
+      default: () => ({ size: 'mini' }),
+    } /* 表格扩展属性 */,
+    operation: {
+      default: () => [
+        ['edit', '编辑', 'el-icon-edit'],
+        ['delete', '删除', 'el-icon-delete', true],
+      ],
+    } /* 操作类型 */,
+    data: Array,
+  },
+  methods: {
+    async handleOper({ event, label, confirm }, data, index, e) {
+      if (e && e.target) {
+        e.target.blur();
+      }
+      try {
+        if (confirm) {
+          const msg = _.isString(confirm) ? confirm : `是否${label}此数据?`;
+          await this.$confirm(msg, '请确认', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            closeOnClickModal: false,
+            type: 'warning',
+          });
+        }
+        this.$emit(event, data, index);
+        this.$emit('oper', { event, data, index });
+      } catch (err) {
+        if (err == 'cancel') {
+          this.$message({
+            type: 'info',
+            message: `已取消${label}`,
+          });
+        }
+      }
+    },
+    selectionChange(selection) {
+      this.$emit('selection', selection);
+    },
+    dblclick(row) {
+      this.$emit('dblclick', row);
+    },
+    handleCurrentChange(val) {
+      this.$emit('currentRow', val);
+    },
+  },
+  computed: {
+    listFields() {
+      const res = this.meta
+        .map(FieldMeta)
+        .filter(p => p.slots.list)
+        .sort((a, b) => a.order - b.order || a.index - b.index)
+        .map(p => ({
+          ...p.field,
+          formatter: Formatter(p, this),
+          options: MergeFilters(p, this),
+        }));
+      // console.log('listFields: ', res);
+      return res;
+    },
+    operItems() {
+      return Operation(this.operation);
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+// .el-table {
+// height: 80vh;
+// border: none;
+// overflow: auto;
+// }
+.el-table::before {
+  height: 0;
+}
+.el-button + .el-button {
+  margin-left: 5px;
+}
+</style>

+ 141 - 0
admin-frame/naf/data/meta-util.js

@@ -0,0 +1,141 @@
+import _ from 'lodash';
+import moment from 'moment';
+
+/* 字段定义 */
+export const FieldMeta = (meta, index) => {
+  let { field, slots, listOpts, dict, pattern } = meta;
+  const { rules, formOpts, order = 0, filterOpts = {} } = meta;
+  filterOpts.order = filterOpts.order || order; // 设置filter字段排序
+  if (field === undefined) {
+    const { name, label, required = false, readonly = false, editable = true, insertable = true } = meta;
+    field = { name, label, required, readonly, editable, insertable };
+  }
+  if (Array.isArray(field)) {
+    const [name, label, required = false, readonly = false, editable = true, insertable = true] = field;
+    field = { name, label, required, readonly, editable, insertable };
+  }
+
+  if (slots === undefined) {
+    // 从meta对象中提取slots属性
+    const { filter = false, list = true, form = true, nonce = false } = meta;
+    slots = { filter, list, form };
+  } else if (Array.isArray(slots)) {
+    slots = {
+      filter: slots.includes('filter'),
+      list: slots.includes('list'),
+      form: slots.includes('form') || slots.includes('form.nonce'),
+      nonce: slots.includes('form.nonce'),
+      ext: slots.includes('ext'),
+    };
+  } else if (_.isObject(slots)) {
+    // 处理slots默认值
+    const { filter = false, list = true, form = true, nonce = false } = slots;
+    slots = { filter, list, form, nonce };
+  }
+
+  // 处理formatter
+  let { formatter } = meta;
+  if (!_.isFunction(formatter) && (_.isObject(formatter) || _.isString(formatter) || Array.isArray(formatter))) {
+    let { name, param } = _.isObject(formatter) ? formatter : {};
+    if (_.isString(formatter)) {
+      [name, param] = formatter.split(':', 2);
+    } else if (Array.isArray(formatter)) {
+      [name, param] = formatter;
+    }
+
+    if (name === undefined) {
+      formatter = undefined;
+    } else if (name === 'dict' && param === undefined) {
+      console.warn(`use ${formatter} formatter must set param,example: 'dict:status' `);
+      formatter = undefined;
+    } else {
+      formatter = { name, param };
+    }
+  }
+
+  return { index, field, rules, slots, order, formOpts, listOpts, filterOpts, formatter, dict, pattern };
+};
+
+/* 列表操作列定义 */
+export const Operation = meta => {
+  let items = Object.entries(meta);
+  if (Array.isArray(meta)) {
+    items = Object.values(meta);
+  }
+  return items.map(item => {
+    if (Array.isArray(item)) {
+      let [event, label, icon, confirm = false] = item;
+      if (_.isBoolean(icon)) {
+        confirm = icon;
+        icon = undefined;
+      }
+      return { event, label, icon, confirm };
+    }
+    return item;
+  });
+};
+
+// 预置formatter函数
+export const formatters = {
+  date: param => (row, column, cellValue, index) => {
+    if (_.isNumber(cellValue) && cellValue <= 0) {
+      return cellValue;
+    }
+    if (cellValue) {
+      const m = moment(cellValue);
+      if (m.isValid()) {
+        return m.format(param || 'YYYY-MM-DD');
+      }
+    }
+    return cellValue;
+  },
+  dict: param => {
+    return function(row, column, cellValue, index) {
+      if (_.isString(cellValue)) {
+        return this.$dict(param, cellValue);
+      }
+      return cellValue;
+    };
+  },
+  bool: param => (row, column, cellValue, index) => {
+    if (cellValue === undefined) {
+      return '';
+    }
+    return cellValue ? '是' : '否';
+  },
+};
+
+export const Formatter = (meta, _this) => {
+  // 处理formatter
+  let { formatter } = meta;
+  if (!_.isFunction(formatter) && _.isObject(formatter)) {
+    const { name, param } = formatter;
+    formatter = formatters[name](param);
+    if (_this !== undefined) {
+      formatter = formatter.bind(_this);
+    }
+  }
+  if (!_.isFunction(formatter)) {
+    formatter = undefined;
+  }
+
+  return formatter;
+};
+
+export const MergeFilters = (meta, _this) => {
+  // 生成column filters
+  let { formatter, listOpts } = meta;
+  if (listOpts && listOpts.filterable && _.isObject(formatter) && formatter.name === 'dict') {
+    let items = _this.$dict(formatter.param);
+    if (!items || items.length > 20) return undefined;
+
+    const filterMethod = (value, row, column) => {
+      const property = column['property'];
+      return row[property] === value;
+    };
+
+    const filters = items.map(p => ({ text: p.name, value: p.code }));
+    return { filters, filterMethod, ...listOpts };
+  }
+  return listOpts;
+};

+ 114 - 0
admin-frame/naf/data/naf-input.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="naf-input">
+    <el-select
+      v-if="dict"
+      v-model="holdValue"
+      :placeholder="placeholder || '请选择'"
+      :disabled="disabled"
+      :size="size"
+      :clearable="clearable"
+      @change="handleChange"
+    >
+      <template v-if="typeof dict == 'function'">
+        <template v-for="(_item, _index) in dict()">
+          <el-option v-if="typeof _item == 'string'" :key="'option-item-' + _index" :label="_item" :value="_item"></el-option>
+          <el-option v-else :key="'option-item-' + _index" :label="_item.name" :value="_item.code" :disabled="_item.status == '1'"></el-option>
+        </template>
+      </template>
+      <template v-else>
+        <template v-for="(_item, _index) in dict">
+          <el-option v-if="typeof _item == 'string'" :key="'option-item-' + _index" :label="_item" :value="_item"></el-option>
+          <el-option v-else :key="'option-item-' + _index" :label="_item.name" :value="_item.code" :disabled="_item.status == '1'"></el-option>
+        </template>
+      </template>
+    </el-select>
+    <el-checkbox v-else-if="type == 'checkbox'" v-model="holdValue" v-bind="options" @change="handleChange">
+      {{ placeholder }}
+    </el-checkbox>
+    <el-date-picker
+      v-else-if="type == 'date' || type == 'datetime'"
+      :type="type"
+      v-model="holdValue"
+      :value-format="valueFormat"
+      v-bind="options"
+      @change="handleChange"
+    >
+    </el-date-picker>
+    <el-input
+      v-else
+      v-model="holdValue"
+      :type="type"
+      v-bind="options"
+      @change="handleChange(true)"
+      @input="handleInput"
+      :minlength="minlength || 0"
+      :maxlength="maxlength || 48"
+    >
+      <template #append>
+        <slot name="append"> </slot>
+      </template>
+      <template #prepend>
+        <slot name="prepend"> </slot>
+      </template>
+      <template #prefix>
+        <slot name="prefix"> </slot>
+      </template>
+      <template #suffix>
+        <slot name="suffix"> </slot>
+      </template>
+    </el-input>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+
+export default {
+  props: {
+    value: { required: true },
+    disabled: Boolean,
+    type: String,
+    dict: [Array, Boolean, Function],
+    placeholder: [String, Boolean],
+    clearable: Boolean,
+    size: String,
+    valueFormat: String,
+    maxlength: Number,
+    minlength: Number,
+  },
+  data() {
+    return {
+      holdValue: this.value,
+    };
+  },
+  computed: {
+    options() {
+      return { placeholder: this.placeholder || undefined, clearable: this.clearable, disabled: this.disabled, size: this.size };
+    },
+  },
+  methods: {
+    handleChange(flag) {
+      if (typeof this.holdValue == 'string' && this.holdValue && this.holdValue != this.holdValue.trim()) {
+        this.holdValue = this.holdValue.trim();
+      }
+      this.$emit('input', this.holdValue);
+      this.$emit('change', this.holdValue);
+    },
+    handleInput() {
+      if (typeof this.holdValue == 'string' && this.holdValue && this.holdValue != this.holdValue.trim()) {
+        this.holdValue = this.holdValue.trim();
+      }
+      this.$emit('input', this.holdValue);
+    },
+  },
+  watch: {
+    value(val) {
+      if (this.holdValue != val) {
+        this.holdValue = val;
+      }
+    },
+  },
+};
+</script>
+
+<style></style>

+ 105 - 0
admin-frame/naf/data/naf-table.vue

@@ -0,0 +1,105 @@
+<template>
+  <el-container class="container">
+    <el-header height="36px" style="line-height:36px;">
+      <div class="filter-box" v-if="filters && filters.length > 0">
+        <el-input placeholder="请输入内容" class="input-with-select" :clearable="true" size="mini" v-model="fieldValue">
+          <el-select slot="prepend" placeholder="请选择" width="110" v-model="fieldName">
+            <el-option v-for="(item, index) in filters" :label="item" :value="item" :key="'filter' + index"></el-option>
+          </el-select>
+          <el-button slot="append" icon="el-icon-search" @click="query"></el-button>
+        </el-input>
+      </div>
+      <el-button icon="el-icon-plus" type="primary" size="mini" v-if="showAction">添加</el-button>
+      <slot name="actionBar"></slot>
+    </el-header>
+    <el-main>
+      <el-table border style="width: 100%;overflow: auto;" size="mini">
+        <slot></slot>
+      </el-table>
+    </el-main>
+    <el-footer height="36px">
+      <el-pagination
+        background
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="page"
+        :page-sizes="[10, 20, 50, 100, 200]"
+        :page-size="size"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+      ></el-pagination>
+    </el-footer>
+  </el-container>
+</template>
+<script>
+export default {
+  name: 'naf-table',
+  props: {
+    dataSource: Array,
+    fields: Array,
+    filters: Array,
+    readonly: Boolean,
+    showQuery: Boolean,
+    showPage: Boolean,
+    total: Number,
+  },
+  data() {
+    return {
+      loading: false,
+      // dataSource: [],
+      // total: 0,
+      page: 1,
+      size: 20,
+      fieldName: '',
+      fieldValue: '',
+      showAction: !this.readonly,
+    };
+  },
+  mounted() {
+    this.query();
+  },
+  methods: {
+    async query() {
+      this.loading = true;
+      setTimeout(() => {
+        this.loading = false;
+      }, 1000);
+    },
+    handleSizeChange(val) {
+      console.log(`每页 ${val} 条`);
+      this.size = val;
+      if (this.total === 0) return;
+      const pages = Math.floor((this.total + val) / val);
+      if (pages < this.page) this.page = pages;
+      this.query();
+    },
+    handleCurrentChange(val) {
+      console.log(`当前页: ${val}`);
+      this.page = val;
+      this.query();
+    },
+  },
+};
+</script>
+<style lang="less">
+.container {
+  width: 100%;
+  height: 100%;
+}
+.main {
+  width: 100%;
+  height: 100%;
+  margin: 0 auto;
+}
+.el-table {
+  height: 100%;
+  overflow: auto;
+}
+.filter-box {
+  width: 280px;
+  display: inline-block;
+  .el-select {
+    width: 100px;
+  }
+}
+</style>

+ 79 - 0
admin-frame/naf/data/wang-editor.vue

@@ -0,0 +1,79 @@
+<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', // 表情
+  'image', // 插入图片
+  'table', // 表格
+  // 'video', // 插入视频
+  // 'code', // 插入代码
+  'undo', // 撤销
+  'redo', // 重复
+];
+
+export default {
+  name: 'wang-editor',
+  model: {
+    prop: 'value',
+    event: 'change', // 默认为input时间,此处改为change
+  },
+  props: {
+    value: { type: String, required: false, default: '' },
+    uploadImgServer: { type: String, required: false, default: '/files/editor/images/upload' },
+  },
+  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 = this.uploadImgServer;
+    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>

+ 12 - 0
admin-frame/naf/error/403.vue

@@ -0,0 +1,12 @@
+<template>
+  <error-page title="403" desc="抱歉,你无权访问该页面" img-url="static/image1.svg"></error-page>
+</template>
+<script>
+import ErrorPage from './error-page';
+
+export default {
+  components: {
+    ErrorPage,
+  },
+};
+</script>

+ 12 - 0
admin-frame/naf/error/404.vue

@@ -0,0 +1,12 @@
+<template>
+  <error-page title="404" desc="抱歉,你访问的页面不存在" img-url="static/image2.svg"></error-page>
+</template>
+<script>
+import ErrorPage from './error-page';
+
+export default {
+  components: {
+    ErrorPage,
+  },
+};
+</script>

+ 12 - 0
admin-frame/naf/error/500.vue

@@ -0,0 +1,12 @@
+<template>
+  <error-page title="500" desc="抱歉,服务器出错了" img-url="static/image3.svg"></error-page>
+</template>
+<script>
+import ErrorPage from './error-page';
+
+export default {
+  components: {
+    ErrorPage,
+  },
+};
+</script>

+ 98 - 0
admin-frame/naf/error/error-page.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="exception">
+      <div class="imgBlock"><div class="imgEle" :style="{backgroundImage: 'url(' + this.imgUrl + ')'}"></div></div>
+      <div class="content">
+        <h1>{{title}}</h1>
+        <div class="desc">{{desc}}</div>
+        <div class="actions">
+          <router-link to="/"><el-button type="primary">返回首页</el-button></router-link>
+        </div>
+      </div>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    title: String,
+    desc: String,
+    'img-url': String,
+  },
+  computed: {
+    styleObject() {
+      return {
+        backgroundImage: `url("${this.imgUrl}")`,
+      };
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.exception {
+  display: flex;
+  align-items: center;
+  height: 100%;
+  font-family: "Helvetica Neue For Number", -apple-system, BlinkMacSystemFont,
+    "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
+    "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+  .imgBlock {
+    flex: 0 0 50%;
+    width: 50%;
+    padding-right: 152px;
+
+    .imgEle {
+      height: 360px;
+      width: 100%;
+      max-width: 430px;
+      float: right;
+      background-repeat: no-repeat;
+      background-position: 50% 50%;
+      background-size: 100% 100%;
+    }
+  }
+
+  .content {
+    flex: auto;
+
+    h1 {
+      color: #434e59;
+      font-size: 72px;
+      font-weight: 600;
+      line-height: 72px;
+      margin-top: 0;
+      margin-bottom: 24px;
+    }
+    .desc {
+      color: rgba(0, 0, 0, 0.45);
+      font-size: 20px;
+      line-height: 28px;
+      margin-bottom: 16px;
+    }
+  }
+}
+@media screen and (max-width: 1200px) {
+  .exception .imgBlock {
+    flex: 0 0 62.5%;
+    width: 62.5%;
+    padding-right: 88px;
+  }
+}
+@media screen and (max-width: 576px) {
+  .exception {
+    display: block;
+    text-align: center;
+  }
+  .exception .imgBlock {
+    padding-right: 0;
+    margin: 0 auto 24px;
+  }
+}
+@media screen and (max-width: 480px) {
+  .exception .imgBlock {
+    margin-bottom: -24px;
+    overflow: hidden;
+  }
+}
+</style>
+

+ 40 - 0
admin-frame/naf/frame/bread.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-breadcrumb separator="/">
+    <el-breadcrumb-item v-if="routerHome" :to="{ path: '/frame/@user/xms/' }">首页</el-breadcrumb-item>
+    <el-breadcrumb-item v-else>首页</el-breadcrumb-item>
+    <el-breadcrumb-item v-for="(item, index) in catalog" :key="index">{{ item }}</el-breadcrumb-item>
+  </el-breadcrumb>
+</template>
+<script>
+const DeepFind = (menus, path) => {
+  for (const k in menus) {
+    const item = menus[k];
+    //if (item.options.path == path) return menus[k];
+    if (path.endsWith(item.options.path)) return menus[k];
+    if (item.children) {
+      const res = DeepFind(item.children, path);
+      if (res) return res;
+    }
+  }
+  return false;
+};
+const routerHome = eval(process.env.VUE_APP_ROUTER_HOME);
+export default {
+  computed: {
+    catalog() {
+      // console.log(this.$route)
+      // return this.$route.meta.catalog;
+      const menu = DeepFind(this.menuItems, this.$route.path);
+      return (menu && menu.options && menu.options.meta && menu.options.meta.catalog) || [];
+    },
+    menuItems() {
+      return this.$store.getters.menuItems;
+    },
+  },
+  data() {
+    return {
+      routerHome,
+    };
+  },
+};
+</script>

+ 30 - 0
admin-frame/naf/frame/footer.vue

@@ -0,0 +1,30 @@
+<template>
+  <span> {{ copyright }}</span>
+</template>
+<script>
+import config from '@frame/config';
+
+export default {
+  data() {
+    return {
+      copyright: config.copyright,
+    };
+  },
+};
+</script>
+<style lang="less">
+.el-footer {
+  height: 48px;
+  line-height: 48px;
+  text-align: center;
+  font-size: 12px;
+  color: #999;
+  background: #fff;
+  -webkit-box-shadow: 4px 4px 40px 0 rgba(0, 0, 0, 0.05);
+  box-shadow: 4px 4px 40px 0 rgba(0, 0, 0, 0.05);
+  width: 100%;
+}
+.footer {
+  border-top: solid 1px #e6e6e6;
+}
+</style>

+ 123 - 0
admin-frame/naf/frame/header/header-dropdown.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="header-box">
+    <div class="logo" :style="{ width: logoWidth }">
+      <router-link to="/">
+        <img src="@/assets/logo1.svg" alt="logo" style="height:32px;width:32px;" />
+      </router-link>
+      <h1>{{ shortName }}</h1>
+    </div>
+    <div class="banner">
+      <i class="naf-icons" :class="{ 'naf-icon-unfold': menuCollapse, 'naf-icon-fold': !menuCollapse }" @click="toggleMenu"></i>
+      <!--下拉用户菜单-->
+      <el-dropdown class="right" @command="handleUserCommand" v-if="false">
+        <span class="el-dropdown-link">
+          <i class="naf-icons naf-icon-avatar"></i>
+          <span class="name">{{ (userinfo && userinfo.fullname) || '管理員' }}</span>
+        </span>
+        <el-dropdown-menu class="action-menu" slot="dropdown">
+          <el-dropdown-item>
+            <i class="naf-icons naf-icon-user"></i>
+            <span>个人中心</span>
+          </el-dropdown-item>
+          <el-dropdown-item>
+            <i class="naf-icons naf-icon-setting"></i>
+            <span>设置</span>
+          </el-dropdown-item>
+          <el-dropdown-item command="logout" divided>
+            <i class="naf-icons naf-icon-quit"></i>
+            <span>退出</span>
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+<script>
+import { mapActions, mapState } from 'vuex';
+
+export default {
+  props: {
+    shortName: String,
+    logoWidth: {
+      type: String,
+      default: '256px',
+    },
+    menuCollapse: Boolean,
+  },
+  methods: {
+    ...mapActions({
+      logout: 'login/logout',
+    }),
+    toggleMenu() {
+      this.$emit('toggle-menu');
+    },
+    async handleUserCommand(command) {
+      if (command === 'logout') {
+        await this.handleLogout();
+      } else {
+        this.$message({
+          type: 'info',
+          message: '即将上线,敬请期待...',
+        });
+      }
+    },
+    async handleLogout() {
+      console.log(this.userinfo);
+      console.log(this);
+      const res = await this.logout();
+      // console.log(res);
+      if (!res.errcode) {
+        this.$router.push(this.$route.query.redirect || '/login');
+      }
+    },
+  },
+  computed: {
+    userinfo() {
+      return this.$store.getters.userinfo;
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.header-box {
+  border-bottom: solid 1px #e6e6e6;
+  display: flex;
+  flex-direction: row;
+  padding: 0;
+  height: 100%;
+  .logo {
+    border-right: solid 1px #e6e6e6;
+    padding: 0 16px;
+    overflow: hidden;
+    img,
+    h1 {
+      vertical-align: middle;
+    }
+    h1 {
+      display: inline-block;
+      margin: 0 0 0 12px;
+    }
+  }
+  .banner {
+    flex: 1;
+    padding: 0 20px;
+    .right {
+      float: right;
+    }
+    .naf-icon-avatar {
+      margin-right: 4px;
+      color: rgba(255, 255, 255, 0.8);
+      font-size: 20px;
+      border-radius: 14px;
+      background: grey;
+      padding: 4px;
+    }
+    .right.lite {
+      font-size: 14px;
+    }
+  }
+}
+.el-button--text {
+  color: white;
+}
+</style>

+ 50 - 0
admin-frame/naf/frame/header/index.vue

@@ -0,0 +1,50 @@
+<template>
+  <div class="header-box">
+    <naf-logo :width="logoWidth" :shortName="shortName" :nav-mode="navMode" @switch-mode="switchMode" :home-route="homeRoute" />
+    <naf-nav-bar v-if="navMode == 'nested'" :menu-items="menuItems" :router-prefix="routerPrefix" />
+    <naf-lite-bar v-else :menu-collapse="menuCollapse" @toggle-menu="toggleMenu" />
+  </div>
+</template>
+<script>
+import NafLogo from './logo';
+import NafLiteBar from './litebar';
+import NafNavBar from './navbar';
+
+export default {
+  components: {
+    NafLogo,
+    NafLiteBar,
+    NafNavBar,
+  },
+  props: {
+    shortName: String,
+    logoWidth: {
+      type: String,
+      default: '256px',
+    },
+    menuCollapse: Boolean,
+    menuItems: Array,
+    navMode: String,
+    routerPrefix: { type: String, default: '' },
+    homeRoute: String,
+  },
+  methods: {
+    switchMode(payload) {
+      this.$emit('switch-mode', payload);
+    },
+    toggleMenu(payload) {
+      this.$emit('toggle-menu', payload);
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.header-box {
+  background: #002766;
+  color: #fff;
+  display: flex;
+  flex-direction: row;
+  padding: 0;
+  height: 100%;
+}
+</style>

+ 32 - 0
admin-frame/naf/frame/header/litebar.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="banner">
+    <i class="naf-icons" :class="{ 'naf-icon-unfold': menuCollapse, 'naf-icon-fold': !menuCollapse }" @click="toggleMenu"></i>
+    <naf-user />
+  </div>
+</template>
+<script>
+import NafUser from './user';
+
+export default {
+  components: {
+    NafUser,
+  },
+  props: {
+    menuCollapse: Boolean,
+  },
+  methods: {
+    toggleMenu() {
+      this.$emit('toggle-menu');
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.banner {
+  flex: 1;
+  display: block;
+  flex-direction: row;
+  align-items: center;
+  padding: 0 20px;
+}
+</style>

+ 39 - 0
admin-frame/naf/frame/header/litebar.vue.bak

@@ -0,0 +1,39 @@
+<template>
+  <div class="banner">
+    <i class="naf-icons icon_btn" :class="{ 'naf-icon-unfold': menuCollapse, 'naf-icon-fold': !menuCollapse }" @click="toggleMenu"></i>
+    <naf-user />
+  </div>
+</template>
+<script>
+import NafUser from './user';
+
+export default {
+  components: {
+    NafUser,
+  },
+  props: {
+    menuCollapse: Boolean,
+  },
+  methods: {
+    toggleMenu() {
+      this.$emit('toggle-menu');
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.banner {
+  // flex: 1;
+  // display: block;
+  // flex-direction: row;
+  // align-items: center;
+  width: 58%;
+  clear: both;
+  padding: 0 20px;
+}
+.icon_btn {
+  display: block;
+  line-height: 7rem;
+  float: left;
+}
+</style>

+ 60 - 0
admin-frame/naf/frame/header/logo.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="logo" :style="{ width: width }">
+    <router-link :to="homeRoute || '/'">
+      <img src="@/assets/logo1.svg" alt="logo" style="height:32px;width:32px;" />
+    </router-link>
+    <h1>{{ shortName }}</h1>
+    <el-tooltip effect="dark" :content="switchTip" placement="bottom" v-if="switchable && !collapse">
+      <el-switch :value="navMode" active-color="#13ce66" active-value="nested" inactive-value="lite" @change="switchMode"> </el-switch>
+    </el-tooltip>
+  </div>
+</template>
+<script>
+import { createNamespacedHelpers } from 'vuex';
+const { mapState } = createNamespacedHelpers('naf/menu');
+
+export default {
+  props: {
+    shortName: String,
+    width: {
+      type: String,
+      default: '256px',
+    },
+    navMode: String,
+    homeRoute: String,
+  },
+  methods: {
+    switchMode(payload) {
+      this.$emit('switch-mode', payload);
+    },
+  },
+  computed: {
+    ...mapState(['collapse', 'switchable']),
+    switchTip() {
+      return this.navMode == 'nested' ? '切换到简单导航模式' : '切换到经典导航模式';
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.logo {
+  display: table;
+  border-right: solid 1px #e6e6e6;
+  padding: 0 16px;
+  overflow: hidden;
+  img,
+  h1 {
+    vertical-align: middle;
+  }
+  h1 {
+    display: inline-block;
+    margin: 0 0 0 12px;
+  }
+
+  .el-switch {
+    vertical-align: middle;
+    text-align: right;
+    display: table-cell;
+  }
+}
+</style>

+ 61 - 0
admin-frame/naf/frame/header/logo.vue.bak

@@ -0,0 +1,61 @@
+<template>
+  <div class="logo">
+    <router-link to="/">
+      <span class="h1">{{ shortName }}</span>
+    </router-link>
+    <el-tooltip effect="dark" :content="switchTip" placement="bottom" v-if="!collapse">
+      <el-switch :value="navMode" active-color="#ff1b14" active-value="nested" inactive-value="lite" @change="switchMode"> </el-switch>
+    </el-tooltip>
+  </div>
+</template>
+<script>
+import { createNamespacedHelpers } from 'vuex';
+const { mapState } = createNamespacedHelpers('naf/menu');
+export default {
+  props: {
+    shortName: String,
+    width: {
+      type: String,
+      default: '256px',
+    },
+    navMode: String,
+  },
+  methods: {
+    switchMode(payload) {
+      this.$emit('switch-mode', payload);
+    },
+  },
+  computed: {
+    ...mapState(['collapse']),
+    switchTip() {
+      return this.navMode == 'nested' ? '切换到简单导航模式' : '切换到经典导航模式';
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.logo {
+  display: table;
+  margin-left: 5%;
+  padding: 0 16px;
+  overflow: hidden;
+  width: 18%;
+  height: 100%;
+  .h1 {
+    color: #000;
+    font-size: 1.5em;
+    width: 70%;
+    float: left;
+    display: block;
+    line-height: 7rem;
+  }
+
+  .el-switch {
+    margin-top: 2.8rem;
+    display: table-cell;
+    width: 10%;
+    float: right;
+    margin-right: 0.5rem;
+  }
+}
+</style>

+ 46 - 0
admin-frame/naf/frame/header/navbar.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="banner">
+    <el-menu class="nav-menu" mode="horizontal" background-color="#20a0ff" text-color="#fff" active-text-color="#ffd04b">
+      <naf-menu-item
+        v-for="(item, idx) in menuItems"
+        :key="idx"
+        :index="idx.toString()"
+        :title="item.title"
+        :options="item.options"
+        :children="item.children"
+        :target="item.target"
+        :prefix="routerPrefix"
+      >
+      </naf-menu-item>
+    </el-menu>
+    <naf-user />
+  </div>
+</template>
+<script>
+import NafMenuItem from '../menu-item';
+import NafUser from './user';
+
+export default {
+  components: {
+    NafMenuItem,
+    NafUser,
+  },
+  props: {
+    menuItems: Array,
+    routerPrefix: { type: String, default: '' },
+  },
+};
+</script>
+<style lang="less" scoped>
+.banner {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 0 20px;
+}
+.nav-menu {
+  flex: 1;
+  border-bottom: 0;
+}
+</style>

+ 60 - 0
admin-frame/naf/frame/header/navbar.vue.bak

@@ -0,0 +1,60 @@
+<template>
+  <div class="banner">
+    <el-menu class="nav-menu" mode="horizontal" background-color="transparent" text-color="#fff" active-text-color="#ffd04b">
+      <naf-menu-item
+        v-for="(item, idx) in menuItems"
+        :key="idx"
+        :index="idx.toString()"
+        :title="item.title"
+        :options="item.options"
+        :children="item.children"
+        :target="item.target"
+        :prefix="routerPrefix"
+      >
+      </naf-menu-item>
+    </el-menu>
+    <naf-user />
+  </div>
+</template>
+<script>
+import NafMenuItem from '../menu-item';
+import NafUser from './user';
+
+export default {
+  components: {
+    NafMenuItem,
+    NafUser,
+  },
+  props: {
+    menuItems: Array,
+    routerPrefix: { type: String, default: '' },
+  },
+};
+</script>
+<style lang="less" scoped>
+.banner {
+  // flex: 1;
+  // display: flex;
+  // flex-direction: row;
+  padding: 0 20px;
+  width: 58%;
+  clear: both;
+}
+.nav-menu {
+  // flex: 1;
+  float: left;
+  border-bottom: 0;
+  background: none;
+  height: 70%;
+  top: 0;
+}
+.el-menu-item {
+  height: 100%;
+  line-height: 6.8rem;
+  font-size: 1rem;
+  font-weight: 600;
+}
+.el-menu-item:hover {
+  background: #21c3f2 !important;
+}
+</style>

+ 213 - 0
admin-frame/naf/frame/header/user.vue

@@ -0,0 +1,213 @@
+<template>
+  <!--简洁用户菜单-->
+  <div class="right lite infos">
+    <i class="infoNumberTow" v-if="items.length > 0"></i>
+    <el-dropdown v-if="userScope" class="user-box" placement="top" @command="handleCommandValue">
+      <el-button type="text" class="name">{{ value || '请选择当前域' }}<i class="el-icon-arrow-down el-icon--right"></i></el-button>
+      <el-dropdown-menu slot="dropdown">
+        <el-dropdown-item :command="item.code" v-for="item in options" :key="item.code">{{ item.name }}</el-dropdown-item>
+      </el-dropdown-menu>
+    </el-dropdown>
+    <span style="margin: 5px;"></span>
+    <el-dropdown class="user-box" placement="top" @command="handleCommand" :hide-on-click="false" @visible-change="change">
+      <el-button type="text" class="name">{{ (userinfo && userinfo.name) || '管理員' }}</el-button>
+      <span style="margin: 5px;">|</span>
+      <el-dropdown-menu slot="dropdown">
+        <el-dropdown-item command="passwd"><i class="el-icon-edit"></i> 修改密码</el-dropdown-item>
+        <!-- <el-dropdown-item><i class="el-icon-chat-dot-round"></i>系统消息</el-dropdown-item> -->
+      </el-dropdown-menu>
+    </el-dropdown>
+    <el-button type="text" @click="handleLogout">退出</el-button>
+    <data-dlg
+      ref="passwd"
+      title="修改密码"
+      width="400px"
+      :visible.sync="passDlg"
+      :data="passForm"
+      :options="{ 'label-width': '80px', size: 'mini' }"
+      :meta="passFields"
+      @save="handleSavePass"
+      @cancel="passDlg = false"
+    >
+    </data-dlg>
+  </div>
+</template>
+<script>
+import { mapActions, mapGetters, createNamespacedHelpers } from 'vuex';
+import DataDlg from '@naf/data/form-dlg.vue';
+
+const { mapState, mapActions: userMapActions } = createNamespacedHelpers('naf/user');
+const userScope = eval(process.env.VUE_APP_USER_SCOPE);
+export default {
+  components: {
+    DataDlg,
+  },
+  data() {
+    const checkPass = ref => {
+      return (rule, value, callback) => {
+        if (value === '') {
+          callback(new Error('请输入新密码'));
+        } else {
+          if (this.$refs[ref].form.confirm !== '') {
+            this.$refs[ref].$refs['form'].validateField('confirm');
+          }
+          callback();
+        }
+      };
+    };
+    const checkConfirm = ref => {
+      return (rule, value, callback) => {
+        if (value === '') {
+          callback(new Error('请再次输入新密码'));
+        } else if (value !== this.$refs[ref].form.newpass) {
+          callback(new Error('两次输入密码不一致!'));
+        } else {
+          callback();
+        }
+      };
+    };
+    return {
+      userScope,
+      options: [],
+      value: '',
+      task: false,
+      taskDlg: false,
+      passDlg: false,
+      passForm: {},
+      passFields: [
+        { name: 'oldpass', label: '原密码', required: true, formOpts: { inputType: 'password' }, rules: [{ required: true, message: '请输入原密码' }] },
+        {
+          name: 'newpass',
+          label: '新密码',
+          required: true,
+          formOpts: { inputType: 'password' },
+          rules: [
+            { required: true, message: '请输入新密码' },
+            { validator: checkPass('passwd'), trigger: 'blur' },
+          ],
+        },
+        {
+          name: 'confirm',
+          label: '密码确认',
+          required: true,
+          formOpts: { inputType: 'password' },
+          rules: [
+            { required: true, message: '请重新输入新密码' },
+            { validator: checkConfirm('passwd'), trigger: 'blur' },
+          ],
+        },
+      ],
+    };
+  },
+  props: {
+    menuCollapse: Boolean,
+  },
+  async mounted() {
+    const res = await this.domains();
+    if (res) {
+      this.options = res.data;
+      if (this.options.length > 0) {
+        const domain = sessionStorage.getItem('x-domain');
+        if (domain) {
+          this.handleCommandValue(domain);
+        } else {
+          this.handleCommandValue(this.options[0].code);
+        }
+      }
+    }
+  },
+  methods: {
+    ...mapActions({
+      logout: 'login/logout',
+      passwd: 'login/passwd',
+    }),
+    ...userMapActions(['taskList', 'domains']),
+    async handleLogout() {
+      const res = await this.logout();
+    },
+    handleCommand(command) {
+      if (command === 'logout') {
+        this.handleLogout();
+      } else if (command == 'infos') {
+        this.taskDlg = true;
+      } else if (command == 'passwd') {
+        this.passDlg = true;
+      }
+    },
+    change(e) {
+      if (e == false) {
+        this.taskDlg = false;
+        this.task = false;
+      }
+    },
+    async handleSavePass(payload) {
+      const res = await this.passwd(payload.data);
+      if (this.$checkRes(res, '修改用户密码成功')) {
+        this.passDlg = false;
+      }
+    },
+    taskBtn() {
+      this.task = true;
+    },
+    handleCommandValue(command) {
+      const val = this.options.filter(p => command === p.code);
+      this.value = val[0].name;
+      window.sessionStorage.setItem('x-domain', command);
+    },
+  },
+  computed: {
+    ...mapGetters(['userinfo']),
+    ...mapState(['items']),
+  },
+};
+</script>
+<style lang="less" scoped>
+.right.lite {
+  font-size: 14px;
+}
+.el-button--text {
+  color: white;
+}
+.name {
+  color: #fff;
+}
+.el-button--text {
+  color: white;
+}
+.user-box {
+  display: inline;
+}
+.infos {
+  position: relative;
+}
+.infoNumberTow {
+  position: absolute;
+  background: #a30202;
+  color: #fff;
+  border-radius: 20px;
+  padding: 5%;
+  line-height: 1.5em;
+  font-size: 0.7em;
+  left: -10%;
+  top: 10%;
+  z-index: 999;
+}
+.infoNumber {
+  position: absolute;
+  background: #a30202;
+  color: #fff;
+  border-radius: 20px;
+  padding: 0 3%;
+  line-height: 1.5em;
+  font-size: 0.7em;
+  left: 2%;
+  top: 0;
+  z-index: 999;
+}
+.userList {
+  width: 480px;
+  position: absolute;
+  left: -490px;
+  top: 0%;
+}
+</style>

+ 77 - 0
admin-frame/naf/frame/menu-item.vue

@@ -0,0 +1,77 @@
+<template>
+  <el-submenu :index="index" v-if="hasChildren">
+    <template slot="title">
+      <i :class="iconCls" v-if="hasIcon"></i>
+      <span slot="title" @click.prevent="menuClick">{{ title }}</span>
+    </template>
+    <naf-menu-item
+      v-for="(item, idx) in children"
+      :key="idx"
+      :index="index + '-' + idx"
+      :title="item.title"
+      :children="item.children"
+      :options="item.options"
+      :prefix="prefix"
+      @naf-menu-item="$emit('naf-menu-item', $event)"
+    >
+    </naf-menu-item>
+  </el-submenu>
+  <el-menu-item :index="index" @click="menuClick" v-else
+    ><i :class="iconCls"></i>
+    <span slot="title" v-if="title.length < 10 && !hasTooltip">{{ title }}</span>
+    <el-tooltip slot="title" v-else :content="hasTooltip ? options.tooltip : title" placement="top" effect="light">
+      <span>{{ title.length > 9 ? title.substr(0, 9) + '...' : title }}</span>
+    </el-tooltip>
+  </el-menu-item>
+</template>
+<script>
+export default {
+  name: 'naf-menu-item',
+  props: {
+    title: String,
+    index: String,
+    options: Object,
+    children: Array,
+    prefix: { type: String, default: '' },
+  },
+  mounted() {
+    let _this = this;
+    //这里可放到全局,提供给子 iframe 调用
+    window.menu = function(to) {
+      _this.$router.push(to);
+    };
+  },
+  methods: {
+    menuClick() {
+      console.debug('click menu item....');
+      if (this.options.url) {
+        window.open(this.options.url, this.options.target);
+      } else if (this.options.path) {
+        const to = `${this.prefix}${this.options.path}`;
+        if (to != (this.$route && this.$route.path)) {
+          this.$router.push(to);
+        } else {
+          this.$emit('naf-menu-item', this.options);
+        }
+      }
+    },
+  },
+  computed: {
+    hasChildren() {
+      return this.children && this.children.length > 0;
+    },
+    hasIcon() {
+      return this.options.icon && this.options.icon.length > 0;
+    },
+    hasTooltip() {
+      return this.options.tooltip && this.options.tooltip.length > 0;
+    },
+    iconCls() {
+      if (this.options.icon && !(this.options.icon.indexOf('el-') === 0)) {
+        return `naf-icons naf-icon-${this.options.icon}`;
+      }
+      return this.options.icon;
+    },
+  },
+};
+</script>

+ 68 - 0
admin-frame/naf/frame/sider.vue

@@ -0,0 +1,68 @@
+<template>
+  <el-menu ref="menu" default-active="0" class="nav-menu" v-bind="themeStyles" :collapse="isCollapse" :router="false" :background-color="backgroundColor">
+    <naf-menu-item
+      v-for="(item, idx) in menuItems"
+      :key="idx"
+      :index="idx.toString()"
+      :title="item.title"
+      :options="item.options"
+      :children="item.children"
+      :target="item.target"
+      :prefix="routerPrefix"
+      @naf-menu-item="$emit('naf-menu-item', $event)"
+    >
+    </naf-menu-item>
+  </el-menu>
+</template>
+<script>
+import NafMenuItem from './menu-item';
+
+export default {
+  components: {
+    NafMenuItem,
+  },
+  props: {
+    theme: String,
+    isCollapse: Boolean,
+    menuItems: Array,
+    routerPrefix: { type: String, default: '' },
+  },
+  data() {
+    return {
+      msg: 'Use Vue 2.0 Today!',
+      // menus,
+    };
+  },
+  // mounted: function() {
+  //   this.$nextTick(_ => {
+  //     // Code that will run only after the
+  //     // entire view has been rendered
+  //     this.$refs.menu.open('0');
+  //   });
+  // },
+  computed: {
+    // 计算属性的 getter
+    themeStyles() {
+      // `this` 指向 vm 实例
+      if (this.theme === 'dark') {
+        // dark styles
+        return {
+          backgroundColor: '#545c64',
+          textColor: '#fff',
+          activeTextColor: '#ffd04b',
+        };
+      }
+      // default styles
+      return {
+        backgroundColor: undefined,
+        textColor: undefined,
+        activeTextColor: undefined,
+      };
+    },
+    backgroundColor() {
+      return this.isCollapse ? '' : 'transparent';
+    },
+  },
+};
+</script>
+<style lang="less"></style>

+ 32 - 0
admin-frame/naf/layouts/footer-page.vue

@@ -0,0 +1,32 @@
+<!--登录页面布局,只有footer-->
+<template>
+  <el-container class="layout" direction="vertical">
+    <el-main style="padding: 0;display: flex;">
+      <transition name="form-fade" mode="in-out">
+        <slot></slot>
+      </transition>
+    </el-main>
+    <!-- <el-footer class="footer" height="layout.footerHeight">
+      <slot name="footer">
+        <naf-footer></naf-footer>
+      </slot>
+    </el-footer> -->
+  </el-container>
+</template>
+<script>
+// import NafFooter from '@naf/frame/footer';
+import config from '@frame/config';
+
+const { layout } = config;
+
+export default {
+  // components: {
+  //   NafFooter,
+  // },
+  data() {
+    return {
+      layout,
+    };
+  },
+};
+</script>

+ 31 - 0
admin-frame/naf/layouts/scroll-page.vue

@@ -0,0 +1,31 @@
+<!--登录页面布局,只有footer-->
+<template functional>
+  <el-scrollbar class="scroll-page">
+    <slot></slot>
+  </el-scrollbar>
+</template>
+
+<style lang="less">
+html,
+body {
+  width: 100%;
+  height: 100%;
+}
+</style>
+<style lang="less">
+.el-scrollbar.scroll-page {
+  height: 100%;
+  width: 100%;
+  & > .el-scrollbar__wrap {
+    overflow-x: hidden;
+    display: flex;
+    & > .el-scrollbar__view {
+      padding: 0px;
+      display: flex;
+      flex: 1;
+      // flex-direction: column;
+      // overflow: auto;
+    }
+  }
+}
+</style>

+ 85 - 0
admin-frame/naf/user/dept-select.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="dept-select">
+    <el-cascader
+      ref="cascader"
+      :value="value"
+      :options="treeItems"
+      :props="created"
+      :size="size"
+      @change="handleChange"
+      @visible-change="handleVisibleChange"
+      :placeholder="placeholder"
+      clearable
+    ></el-cascader>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'dept-select',
+  props: {
+    props: Object,
+    size: { type: String, default: 'small' },
+    navItems: Array,
+    dataItems: Array,
+    value: { required: false },
+    placeholder: { type: String, default: '请选择用户部门' },
+  },
+  data() {
+    return {
+      cascaderProps: {
+        multiple: false,
+        checkStrictly: true,
+        emitPath: false,
+        children: 'children',
+        label: 'name',
+        value: 'id',
+      },
+      selectedValue: this.value,
+      panelVisible: false,
+    };
+  },
+  computed: {
+    treeItems() {
+      if (this.navItems) return this.navItems;
+      const items = this.dataItems || [];
+      const root = items.filter(a => !items.some(b => b.id === a.pid));
+      const fetchChildren = item => {
+        if (!item.path) item.path = [item.name];
+        const children = items.filter(p => p.pid === item.id).map(p => fetchChildren({ ...p, parent: item, path: item.path.concat(p.name) }));
+        if (children && children.length > 0) return { ...item, children };
+        return { ...item, leaf: true };
+      };
+      return root.map(p => fetchChildren(p));
+    },
+    created() {
+      const cascaderProps = { ...this.cascaderProps, ...(this.props || {}) };
+      return cascaderProps;
+    },
+  },
+  methods: {
+    handleChange(payload) {
+      this.selectedValue = payload;
+      this.$emit('input', payload);
+      this.$emit('change', payload);
+      if (!this.panelVisible) {
+        this.$emit('select', this.selectedValue);
+      }
+    },
+    handleVisibleChange(visible) {
+      this.panelVisible = visible;
+      if (visible === false) {
+        this.$emit('select', this.selectedValue);
+      }
+    },
+    toggleDropDownVisible(visible) {
+      this.$refs.cascader.toggleDropDownVisible(visible);
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.el-cascader {
+  width: 100%;
+}
+</style>

+ 146 - 0
admin-frame/naf/user/dept-tree.vue

@@ -0,0 +1,146 @@
+<template>
+  <div class="nav-tree">
+    <el-input class="filter" v-model="filter" :placeholder="placeholder" size="mini" clearable></el-input>
+    <el-tree
+      ref="tree"
+      :show-checkbox="showCheckbox"
+      :getCheckedNodes="true"
+      :data="treeItems"
+      :props="navProps"
+      node-key="id"
+      @node-click="handleNavClick"
+      @check-change="handleCheckChange"
+      :highlight-current="true"
+      :expand-on-click-node="false"
+    >
+      <template v-slot="{ node, data }">
+        <slot>
+          <span v-if="node.label.length <= 8 - node.level" class="dept-tree-node">
+            <i :class="iconCls(data.icon || 'folder')" style="margin-right: 5px;" v-if="showIcon"></i>
+            <span>{{ node.label }}</span>
+            <el-badge :value="data.desc" class="item" type="info"></el-badge>
+          </span>
+          <el-tooltip v-else effect="dark" :content="data.desc ? node.label + '(' + data.desc + ')' : node.label" placement="right">
+            <span class="dept-tree-node">
+              <i :class="iconCls(data.icon || 'folder')" style="margin-right: 5px;" v-if="showIcon"></i>
+              <span>{{ node.label }}</span>
+              <el-badge :value="data.desc" class="item" type="info"></el-badge>
+            </span>
+          </el-tooltip>
+        </slot>
+      </template>
+    </el-tree>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    navItems: Array,
+    dataItems: Array,
+    virtualRoot: String,
+    showIcon: { type: Boolean, default: false },
+    placeholder: String,
+    showCheckbox: { type: Boolean, default: false },
+    keys: Array,
+  },
+  data() {
+    return {
+      navProps: {
+        children: 'children',
+        label: 'name',
+      },
+      filter: null,
+    };
+  },
+  methods: {
+    handleCheckChange() {
+      const data = this.$refs.tree.getCheckedNodes();
+      this.$emit('checkchange', data);
+    },
+    handleNavClick(data) {
+      this.$emit('selected', data && !data.isRoot ? data : null);
+    },
+    setCurrentKey(key) {
+      this.$refs['tree'].setCurrentKey(key);
+    },
+    iconCls(icon) {
+      if (icon && !(icon.indexOf('el-') === 0)) {
+        return `naf-icons naf-icon-${icon}`;
+      }
+      return icon;
+    },
+  },
+  computed: {
+    treeItems() {
+      if (this.filter) {
+        return this.dataItems.filter(p => p.name && p.name.includes(this.filter));
+      } else {
+        if (this.navItems) return this.navItems;
+        const items = this.dataItems || [];
+        let roots = items.filter(a => !items.some(b => b.id === a.pid));
+        const fetchChildren = item => {
+          if (!item.path) item.path = [item.name];
+          const children = items.filter(p => p.pid === item.id).map(p => fetchChildren({ ...p, parent: item, path: item.path.concat(p.name) }));
+          return { ...item, children };
+        };
+        roots = roots.map(p => fetchChildren(p));
+        if (!this.virtualRoot) {
+          return roots;
+        }
+        // 显示虚拟根目录
+        return [{ [this.navProps.label]: this.virtualRoot, children: roots, isRoot: true, icon: 'dept' }];
+      }
+    },
+  },
+  watch: {
+    keys(val) {
+      this.$refs.tree.setCheckedKeys(val);
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.item {
+  margin-top: 6px;
+  margin-left: 5px;
+}
+.nav-tree {
+  width: 100%;
+  overflow-x: auto;
+  /deep/ .el-tree-node.is-current > .el-tree-node__content {
+    background-color: #409eff;
+    color: white;
+    .naf-icon-dian {
+      display: inline;
+      color: white;
+    }
+  }
+  /deep/ .naf-icon-dian {
+    display: none;
+  }
+  /deep/ .el-tree-node__content:hover .naf-icon-dian {
+    display: inline;
+  }
+  .filter {
+    display: block;
+    position: relative;
+    width: 90%;
+    margin: 0 auto;
+    margin-bottom: 10px;
+    margin-top: 10px;
+  }
+  .el-tree {
+    width: 100%;
+    overflow: hidden;
+  }
+  /deep/ .dept-tree-node {
+    overflow: hidden;
+    display: flex;
+    .el-tooltip {
+      overflow: hidden;
+      width: 100%;
+      display: inline-grid;
+    }
+  }
+}
+</style>

+ 150 - 0
admin-frame/naf/user/user-select.vue

@@ -0,0 +1,150 @@
+<template>
+  <el-dialog :title="title" :visible.sync="visible" width="600px" :close-on-click-modal="false">
+    <div class="content">
+      <div class="left">
+        <el-scrollbar>
+          <el-tree ref="tree" :data="treeItems" :props="navProps" node-key="_id" @node-click="handleNavClick" :highlight-current="true">
+            <span class="custom-tree-node" slot-scope="{ node, data }">
+              <span>
+                <i class="prefix naf-icons naf-icon-user1" v-if="data.userid" />
+                <i class="prefix naf-icons naf-icon-folder1" v-else />
+                {{ node.label }}
+              </span>
+              <span>
+                <i class="suffix naf-icons naf-icon-ok" v-show="inSelected(data)" />
+              </span>
+            </span>
+          </el-tree>
+        </el-scrollbar>
+      </div>
+      <div class="right">
+        <el-scrollbar>
+          <el-tree ref="selected" :data="selected" :props="navProps" node-key="id" :highlight-current="true">
+            <span class="custom-tree-node" slot-scope="{ node, data }">
+              <span>
+                <i class="prefix naf-icons naf-icon-user1" v-if="data.userid" />
+                <i class="prefix naf-icons naf-icon-folder1" v-else />
+                {{ node.label }}
+              </span>
+              <span style="float: right">
+                <i class="suffix naf-icons naf-icon-close" />
+              </span>
+            </span>
+          </el-tree>
+        </el-scrollbar>
+      </div>
+    </div>
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="$emit('cancel')" :size="options.size">取 消</el-button>
+      <el-button type="primary" @click="$emit('ok', selected)" :size="options.size">确 定</el-button>
+    </div>
+  </el-dialog>
+</template>
+<script>
+import { mapState } from 'vuex';
+import _ from 'lodash';
+
+export default {
+  props: {
+    options: {
+      type: Object,
+      default: () => ({ size: 'small' }),
+    } /* form options */,
+    title: String,
+    visible: { type: Boolean, default: true },
+  },
+  data() {
+    return {
+      navProps: {
+        children: 'children',
+        label: 'name',
+      },
+      selected: [],
+    };
+  },
+  methods: {
+    handleNavClick(data) {
+      // this.$emit('selected', data);
+      const idx = this.selected.findIndex(p => (_.isNumber(data.id) && p.id === data.id) || (_.isString(data.userid) && p.userid === data.userid));
+      console.log('idx:', idx);
+      if (idx === -1) {
+        this.selected.push(_.omit(data, ['children']));
+      } else {
+        this.selected.splice(idx, 1);
+      }
+    },
+    inSelected(data) {
+      return this.selected.some(p => (_.isNumber(data.id) && p.id === data.id) || (_.isString(data.userid) && p.userid === data.userid));
+    },
+  },
+  computed: {
+    ...mapState({
+      deptItems: state => state.system.dept.items,
+      userItems: state => state.system.user.items,
+    }),
+    treeItems() {
+      if (this.navItems) return this.navItems;
+      const items = this.deptItems || [];
+      const root = items.filter(a => !items.some(b => b.id === a.parentid));
+      const fetchChildren = item => {
+        const children = items.filter(p => p.parentid === item.id).map(p => fetchChildren(p));
+        const users = this.userItems.filter(p => p.department && p.department.includes(item.id));
+        return { ...item, children: [...children, ...users] };
+      };
+      const rootUsers = this.userItems.filter(p => p.department && p.department.includes(0));
+      return root.map(p => fetchChildren(p)).concat(rootUsers);
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.nav-tree {
+  /deep/ .el-tree-node.is-current > .el-tree-node__content {
+    background-color: #409eff;
+    color: white;
+    .naf-icon-dian {
+      display: inline;
+      color: white;
+    }
+  }
+  /deep/ .naf-icon-dian {
+    display: none;
+  }
+  /deep/ .el-tree-node__content:hover .naf-icon-dian {
+    display: inline;
+  }
+}
+
+.content {
+  display: flex;
+  flex-direction: row;
+  height: 340px;
+}
+
+.left {
+  flex: 1;
+}
+.right {
+  width: 240px;
+}
+
+.naf-icons.prefix {
+  color: grey;
+}
+.naf-icons.suffix {
+  color: lightgrey;
+}
+
+.custom-tree-node {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  padding-right: 8px;
+}
+
+.el-scrollbar {
+  height: 100%;
+}
+</style>

+ 58 - 0
admin-frame/package.json

@@ -0,0 +1,58 @@
+{
+  "name": "admin-frame",
+  "version": "1.0.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "serve-54": "vue-cli-service serve --mode 54d",
+    "build-54": "vue-cli-service build --mode 54",
+    "serve-51": "vue-cli-service serve --mode 51",
+    "build-51": "vue-cli-service build --mode 51",
+    "serve-hj": "vue-cli-service serve --mode hj",
+    "build-hj": "vue-cli-service build --mode hj",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@stomp/stompjs": "^5.4.4",
+    "axios": "^0.19.2",
+    "core-js": "^3.6.5",
+    "element-ui": "^2.13.2",
+    "js-cookie": "^2.2.1",
+    "jsonwebtoken": "^8.5.1",
+    "moment": "^2.27.0",
+    "naf-core": "0.1.2",
+    "qrcode": "^1.4.4",
+    "url-join": "^4.0.1",
+    "uuidjs": "^4.2.6",
+    "vue": "^2.6.11",
+    "vue-meta": "^2.4.0",
+    "vue-router": "^3.4.3",
+    "vuex": "^3.5.1"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "^4.5.4",
+    "@vue/cli-plugin-eslint": "^4.5.4",
+    "@vue/cli-service": "^4.5.4",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "babel-eslint": "^10.1.0",
+    "babel-plugin-component": "^1.1.1",
+    "eslint": "^7.7.0",
+    "eslint-plugin-prettier": "^3.1.4",
+    "eslint-plugin-vue": "^6.2.2",
+    "less": "^3.12.2",
+    "less-loader": "^6.2.0",
+    "vue-cli-plugin-element": "^1.0.1",
+    "vue-template-compiler": "^2.6.11"
+  },
+  "postcss": {
+    "plugins": {
+      "autoprefixer": {}
+    }
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ]
+}

+ 0 - 0
admin-frame/public/.gitkeep


BIN
admin-frame/public/favicon.ico


+ 19 - 0
admin-frame/public/index.html

@@ -0,0 +1,19 @@
+<!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">
+    <link rel="stylesheet" href="<%= BASE_URL %>naf-icons/iconfont.css">
+    <link rel="stylesheet" href="<%= BASE_URL %>weui/weui.min.css">
+    <title>加载中...</title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but admin-frame 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>

+ 539 - 0
admin-frame/public/naf-icons/demo.css

@@ -0,0 +1,539 @@
+/* Logo 字体 */
+@font-face {
+  font-family: "iconfont logo";
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
+}
+
+.logo {
+  font-family: "iconfont logo";
+  font-size: 160px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* tabs */
+.nav-tabs {
+  position: relative;
+}
+
+.nav-tabs .nav-more {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  height: 42px;
+  line-height: 42px;
+  color: #666;
+}
+
+#tabs {
+  border-bottom: 1px solid #eee;
+}
+
+#tabs li {
+  cursor: pointer;
+  width: 100px;
+  height: 40px;
+  line-height: 40px;
+  text-align: center;
+  font-size: 16px;
+  border-bottom: 2px solid transparent;
+  position: relative;
+  z-index: 1;
+  margin-bottom: -1px;
+  color: #666;
+}
+
+
+#tabs .active {
+  border-bottom-color: #f00;
+  color: #222;
+}
+
+.tab-container .content {
+  display: none;
+}
+
+/* 页面布局 */
+.main {
+  padding: 30px 100px;
+  width: 960px;
+  margin: 0 auto;
+}
+
+.main .logo {
+  color: #333;
+  text-align: left;
+  margin-bottom: 30px;
+  line-height: 1;
+  height: 110px;
+  margin-top: -50px;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.main .logo a {
+  font-size: 160px;
+  color: #333;
+}
+
+.helps {
+  margin-top: 40px;
+}
+
+.helps pre {
+  padding: 20px;
+  margin: 10px 0;
+  border: solid 1px #e7e1cd;
+  background-color: #fffdef;
+  overflow: auto;
+}
+
+.icon_lists {
+  width: 100% !important;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.icon_lists li {
+  width: 100px;
+  margin-bottom: 10px;
+  margin-right: 20px;
+  text-align: center;
+  list-style: none !important;
+  cursor: default;
+}
+
+.icon_lists li .code-name {
+  line-height: 1.2;
+}
+
+.icon_lists .icon {
+  display: block;
+  height: 100px;
+  line-height: 100px;
+  font-size: 42px;
+  margin: 10px auto;
+  color: #333;
+  -webkit-transition: font-size 0.25s linear, width 0.25s linear;
+  -moz-transition: font-size 0.25s linear, width 0.25s linear;
+  transition: font-size 0.25s linear, width 0.25s linear;
+}
+
+.icon_lists .icon:hover {
+  font-size: 100px;
+}
+
+.icon_lists .svg-icon {
+  /* 通过设置 font-size 来改变图标大小 */
+  width: 1em;
+  /* 图标和文字相邻时,垂直对齐 */
+  vertical-align: -0.15em;
+  /* 通过设置 color 来改变 SVG 的颜色/fill */
+  fill: currentColor;
+  /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
+      normalize.css 中也包含这行 */
+  overflow: hidden;
+}
+
+.icon_lists li .name,
+.icon_lists li .code-name {
+  color: #666;
+}
+
+/* markdown 样式 */
+.markdown {
+  color: #666;
+  font-size: 14px;
+  line-height: 1.8;
+}
+
+.highlight {
+  line-height: 1.5;
+}
+
+.markdown img {
+  vertical-align: middle;
+  max-width: 100%;
+}
+
+.markdown h1 {
+  color: #404040;
+  font-weight: 500;
+  line-height: 40px;
+  margin-bottom: 24px;
+}
+
+.markdown h2,
+.markdown h3,
+.markdown h4,
+.markdown h5,
+.markdown h6 {
+  color: #404040;
+  margin: 1.6em 0 0.6em 0;
+  font-weight: 500;
+  clear: both;
+}
+
+.markdown h1 {
+  font-size: 28px;
+}
+
+.markdown h2 {
+  font-size: 22px;
+}
+
+.markdown h3 {
+  font-size: 16px;
+}
+
+.markdown h4 {
+  font-size: 14px;
+}
+
+.markdown h5 {
+  font-size: 12px;
+}
+
+.markdown h6 {
+  font-size: 12px;
+}
+
+.markdown hr {
+  height: 1px;
+  border: 0;
+  background: #e9e9e9;
+  margin: 16px 0;
+  clear: both;
+}
+
+.markdown p {
+  margin: 1em 0;
+}
+
+.markdown>p,
+.markdown>blockquote,
+.markdown>.highlight,
+.markdown>ol,
+.markdown>ul {
+  width: 80%;
+}
+
+.markdown ul>li {
+  list-style: circle;
+}
+
+.markdown>ul li,
+.markdown blockquote ul>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown>ul li p,
+.markdown>ol li p {
+  margin: 0.6em 0;
+}
+
+.markdown ol>li {
+  list-style: decimal;
+}
+
+.markdown>ol li,
+.markdown blockquote ol>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown code {
+  margin: 0 3px;
+  padding: 0 5px;
+  background: #eee;
+  border-radius: 3px;
+}
+
+.markdown strong,
+.markdown b {
+  font-weight: 600;
+}
+
+.markdown>table {
+  border-collapse: collapse;
+  border-spacing: 0px;
+  empty-cells: show;
+  border: 1px solid #e9e9e9;
+  width: 95%;
+  margin-bottom: 24px;
+}
+
+.markdown>table th {
+  white-space: nowrap;
+  color: #333;
+  font-weight: 600;
+}
+
+.markdown>table th,
+.markdown>table td {
+  border: 1px solid #e9e9e9;
+  padding: 8px 16px;
+  text-align: left;
+}
+
+.markdown>table th {
+  background: #F7F7F7;
+}
+
+.markdown blockquote {
+  font-size: 90%;
+  color: #999;
+  border-left: 4px solid #e9e9e9;
+  padding-left: 0.8em;
+  margin: 1em 0;
+}
+
+.markdown blockquote p {
+  margin: 0;
+}
+
+.markdown .anchor {
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  margin-left: 8px;
+}
+
+.markdown .waiting {
+  color: #ccc;
+}
+
+.markdown h1:hover .anchor,
+.markdown h2:hover .anchor,
+.markdown h3:hover .anchor,
+.markdown h4:hover .anchor,
+.markdown h5:hover .anchor,
+.markdown h6:hover .anchor {
+  opacity: 1;
+  display: inline-block;
+}
+
+.markdown>br,
+.markdown>p>br {
+  clear: both;
+}
+
+
+.hljs {
+  display: block;
+  background: white;
+  padding: 0.5em;
+  color: #333333;
+  overflow-x: auto;
+}
+
+.hljs-comment,
+.hljs-meta {
+  color: #969896;
+}
+
+.hljs-string,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-strong,
+.hljs-emphasis,
+.hljs-quote {
+  color: #df5000;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-type {
+  color: #a71d5d;
+}
+
+.hljs-literal,
+.hljs-symbol,
+.hljs-bullet,
+.hljs-attribute {
+  color: #0086b3;
+}
+
+.hljs-section,
+.hljs-name {
+  color: #63a35c;
+}
+
+.hljs-tag {
+  color: #333333;
+}
+
+.hljs-title,
+.hljs-attr,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-selector-attr,
+.hljs-selector-pseudo {
+  color: #795da3;
+}
+
+.hljs-addition {
+  color: #55a532;
+  background-color: #eaffea;
+}
+
+.hljs-deletion {
+  color: #bd2c00;
+  background-color: #ffecec;
+}
+
+.hljs-link {
+  text-decoration: underline;
+}
+
+/* 代码高亮 */
+/* PrismJS 1.15.0
+https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+code[class*="language-"],
+pre[class*="language-"] {
+  color: black;
+  background: none;
+  text-shadow: 0 1px white;
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+  text-align: left;
+  white-space: pre;
+  word-spacing: normal;
+  word-break: normal;
+  word-wrap: normal;
+  line-height: 1.5;
+
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+
+  -webkit-hyphens: none;
+  -moz-hyphens: none;
+  -ms-hyphens: none;
+  hyphens: none;
+}
+
+pre[class*="language-"]::-moz-selection,
+pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection,
+code[class*="language-"] ::-moz-selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+pre[class*="language-"]::selection,
+pre[class*="language-"] ::selection,
+code[class*="language-"]::selection,
+code[class*="language-"] ::selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+@media print {
+
+  code[class*="language-"],
+  pre[class*="language-"] {
+    text-shadow: none;
+  }
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+  padding: 1em;
+  margin: .5em 0;
+  overflow: auto;
+}
+
+:not(pre)>code[class*="language-"],
+pre[class*="language-"] {
+  background: #f5f2f0;
+}
+
+/* Inline code */
+:not(pre)>code[class*="language-"] {
+  padding: .1em;
+  border-radius: .3em;
+  white-space: normal;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: slategray;
+}
+
+.token.punctuation {
+  color: #999;
+}
+
+.namespace {
+  opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+  color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+  color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+  color: #9a6e3a;
+  background: hsla(0, 0%, 100%, .5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+  color: #07a;
+}
+
+.token.function,
+.token.class-name {
+  color: #DD4A68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+  color: #e90;
+}
+
+.token.important,
+.token.bold {
+  font-weight: bold;
+}
+
+.token.italic {
+  font-style: italic;
+}
+
+.token.entity {
+  cursor: help;
+}

+ 850 - 0
admin-frame/public/naf-icons/demo_fontclass.html

@@ -0,0 +1,850 @@
+
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8"/>
+    <title>IconFont</title>
+    <link rel="stylesheet" href="demo.css">
+    <link rel="stylesheet" href="iconfont.css">
+</head>
+<body>
+    <div class="main markdown">
+        <h1>IconFont 图标</h1>
+        <ul class="icon_lists clear">
+            
+                <li>
+                <i class="icon naf-icons naf-icon-xinxi"></i>
+                    <div class="name">信息</div>
+                    <div class="fontclass">.naf-icon-xinxi</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-avatar"></i>
+                    <div class="name">user</div>
+                    <div class="fontclass">.naf-icon-avatar</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-idok"></i>
+                    <div class="name">身份证号</div>
+                    <div class="fontclass">.naf-icon-idok</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-folder"></i>
+                    <div class="name">cc-folder</div>
+                    <div class="fontclass">.naf-icon-folder</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfont5"></i>
+                    <div class="name">iconfont-5</div>
+                    <div class="fontclass">.naf-icon-iconfont5</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-doc1"></i>
+                    <div class="name">发文</div>
+                    <div class="fontclass">.naf-icon-doc1</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wxbbaobiao"></i>
+                    <div class="name">wxb报表</div>
+                    <div class="fontclass">.naf-icon-wxbbaobiao</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wxbbiaowang"></i>
+                    <div class="name">wxb标王</div>
+                    <div class="fontclass">.naf-icon-wxbbiaowang</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wxbgongju"></i>
+                    <div class="name">wxb工具</div>
+                    <div class="fontclass">.naf-icon-wxbgongju</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wxbmingxingdianpu"></i>
+                    <div class="name">wxb明星店铺</div>
+                    <div class="fontclass">.naf-icon-wxbmingxingdianpu</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wxbpinpaibao"></i>
+                    <div class="name">wxb品牌宝</div>
+                    <div class="fontclass">.naf-icon-wxbpinpaibao</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-idcard"></i>
+                    <div class="name">wxb账户</div>
+                    <div class="fontclass">.naf-icon-idcard</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wxbzhuye"></i>
+                    <div class="name">wxb主页</div>
+                    <div class="fontclass">.naf-icon-wxbzhuye</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wxbsousuotuiguang"></i>
+                    <div class="name">wxb搜索推广</div>
+                    <div class="fontclass">.naf-icon-wxbsousuotuiguang</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wxbdingwei"></i>
+                    <div class="name">wxb定位</div>
+                    <div class="fontclass">.naf-icon-wxbdingwei</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-jifen"></i>
+                    <div class="name">积分</div>
+                    <div class="fontclass">.naf-icon-jifen</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-erweima"></i>
+                    <div class="name">二维码</div>
+                    <div class="fontclass">.naf-icon-erweima</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-jushoucang"></i>
+                    <div class="name">聚收藏</div>
+                    <div class="fontclass">.naf-icon-jushoucang</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-lajixiang"></i>
+                    <div class="name">垃圾箱</div>
+                    <div class="fontclass">.naf-icon-lajixiang</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-lianjie"></i>
+                    <div class="name">链接</div>
+                    <div class="fontclass">.naf-icon-lianjie</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-naozhong"></i>
+                    <div class="name">闹钟</div>
+                    <div class="fontclass">.naf-icon-naozhong</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-saoyisao"></i>
+                    <div class="name">扫一扫</div>
+                    <div class="fontclass">.naf-icon-saoyisao</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-shezhi"></i>
+                    <div class="name">设置</div>
+                    <div class="fontclass">.naf-icon-shezhi</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-shengyin"></i>
+                    <div class="name">声音</div>
+                    <div class="fontclass">.naf-icon-shengyin</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-shijian"></i>
+                    <div class="name">时间</div>
+                    <div class="fontclass">.naf-icon-shijian</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-location"></i>
+                    <div class="name">收货地址</div>
+                    <div class="fontclass">.naf-icon-location</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-shouye"></i>
+                    <div class="name">首页</div>
+                    <div class="fontclass">.naf-icon-shouye</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-sousuo"></i>
+                    <div class="name">搜索</div>
+                    <div class="fontclass">.naf-icon-sousuo</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-suo"></i>
+                    <div class="name"></div>
+                    <div class="fontclass">.naf-icon-suo</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-tishi"></i>
+                    <div class="name">提示</div>
+                    <div class="fontclass">.naf-icon-tishi</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wancheng"></i>
+                    <div class="name">完成</div>
+                    <div class="fontclass">.naf-icon-wancheng</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wodedingdan"></i>
+                    <div class="name">我的订单</div>
+                    <div class="fontclass">.naf-icon-wodedingdan</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-msg2"></i>
+                    <div class="name">我的反馈</div>
+                    <div class="fontclass">.naf-icon-msg2</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wodehongbao"></i>
+                    <div class="name">我的红包</div>
+                    <div class="fontclass">.naf-icon-wodehongbao</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wodejuhuasuan"></i>
+                    <div class="name">我的聚划算</div>
+                    <div class="fontclass">.naf-icon-wodejuhuasuan</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-wodeyouhuiquan"></i>
+                    <div class="name">我的优惠券</div>
+                    <div class="fontclass">.naf-icon-wodeyouhuiquan</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-xiala"></i>
+                    <div class="name">下拉</div>
+                    <div class="fontclass">.naf-icon-xiala</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-yanjing"></i>
+                    <div class="name">眼睛</div>
+                    <div class="fontclass">.naf-icon-yanjing</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-receipt"></i>
+                    <div class="name">意见反馈</div>
+                    <div class="fontclass">.naf-icon-receipt</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-zhaoxiangji"></i>
+                    <div class="name">照相机</div>
+                    <div class="fontclass">.naf-icon-zhaoxiangji</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-ok"></i>
+                    <div class="name">正确</div>
+                    <div class="fontclass">.naf-icon-ok</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-msg1"></i>
+                    <div class="name">消息中心</div>
+                    <div class="fontclass">.naf-icon-msg1</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-lingcunwei"></i>
+                    <div class="name">另存为</div>
+                    <div class="fontclass">.naf-icon-lingcunwei</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-new"></i>
+                    <div class="name">new</div>
+                    <div class="fontclass">.naf-icon-new</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-huo"></i>
+                    <div class="name"></div>
+                    <div class="fontclass">.naf-icon-huo</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-password"></i>
+                    <div class="name">password</div>
+                    <div class="fontclass">.naf-icon-password</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-doc4"></i>
+                    <div class="name">收文</div>
+                    <div class="fontclass">.naf-icon-doc4</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontyouhuiquan"></i>
+                    <div class="name">iconfont-youhuiquan</div>
+                    <div class="fontclass">.naf-icon-iconfontyouhuiquan</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontxingxing"></i>
+                    <div class="name">iconfont-xingxing</div>
+                    <div class="fontclass">.naf-icon-iconfontxingxing</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontmingpian"></i>
+                    <div class="name">iconfont-mingpian</div>
+                    <div class="fontclass">.naf-icon-iconfontmingpian</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-mobile"></i>
+                    <div class="name">iconfont-shouji</div>
+                    <div class="fontclass">.naf-icon-mobile</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontriyongbaihuo"></i>
+                    <div class="name">iconfont-riyongbaihuo</div>
+                    <div class="fontclass">.naf-icon-iconfontriyongbaihuo</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-setting"></i>
+                    <div class="name">iconfont-jixieqimo</div>
+                    <div class="fontclass">.naf-icon-setting</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontxiangjiaosuliao"></i>
+                    <div class="name">iconfont-xiangjiaosuliao</div>
+                    <div class="fontclass">.naf-icon-iconfontxiangjiaosuliao</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontyiqiyibiao"></i>
+                    <div class="name">iconfont-yiqiyibiao</div>
+                    <div class="fontclass">.naf-icon-iconfontyiqiyibiao</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontzhaomingdianzi"></i>
+                    <div class="name">iconfont-zhaomingdianzi</div>
+                    <div class="fontclass">.naf-icon-iconfontzhaomingdianzi</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontfuwushichang"></i>
+                    <div class="name">iconfont-fuwushichang</div>
+                    <div class="fontclass">.naf-icon-iconfontfuwushichang</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontfangzhipige"></i>
+                    <div class="name">iconfont-fangzhipige</div>
+                    <div class="fontclass">.naf-icon-iconfontfangzhipige</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-iconfontbaozhuang"></i>
+                    <div class="name">iconfont-baozhuang</div>
+                    <div class="fontclass">.naf-icon-iconfontbaozhuang</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-gerenshimingrz"></i>
+                    <div class="name">个人实名认证</div>
+                    <div class="fontclass">.naf-icon-gerenshimingrz</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-renzheng"></i>
+                    <div class="name">企业名称认证</div>
+                    <div class="fontclass">.naf-icon-renzheng</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-shendurz"></i>
+                    <div class="name">深度认证</div>
+                    <div class="fontclass">.naf-icon-shendurz</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-clearup"></i>
+                    <div class="name">Clearup</div>
+                    <div class="fontclass">.naf-icon-clearup</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-site"></i>
+                    <div class="name">网站</div>
+                    <div class="fontclass">.naf-icon-site</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-jushoucanggift"></i>
+                    <div class="name">聚收藏gift</div>
+                    <div class="fontclass">.naf-icon-jushoucanggift</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-liwu"></i>
+                    <div class="name">礼物</div>
+                    <div class="fontclass">.naf-icon-liwu</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-yuyin"></i>
+                    <div class="name">语音</div>
+                    <div class="fontclass">.naf-icon-yuyin</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-user"></i>
+                    <div class="name">user2</div>
+                    <div class="fontclass">.naf-icon-user</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-zhengshu"></i>
+                    <div class="name">证书</div>
+                    <div class="fontclass">.naf-icon-zhengshu</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-user1"></i>
+                    <div class="name">User</div>
+                    <div class="fontclass">.naf-icon-user1</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-feedback"></i>
+                    <div class="name">feedback</div>
+                    <div class="fontclass">.naf-icon-feedback</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-doc3"></i>
+                    <div class="name">发文</div>
+                    <div class="fontclass">.naf-icon-doc3</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-job"></i>
+                    <div class="name">招聘 </div>
+                    <div class="fontclass">.naf-icon-job</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-cert"></i>
+                    <div class="name">证书</div>
+                    <div class="fontclass">.naf-icon-cert</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-all"></i>
+                    <div class="name">all</div>
+                    <div class="fontclass">.naf-icon-all</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-cart"></i>
+                    <div class="name">cart</div>
+                    <div class="fontclass">.naf-icon-cart</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-set"></i>
+                    <div class="name">set</div>
+                    <div class="fontclass">.naf-icon-set</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-arrowdown"></i>
+                    <div class="name">arrow-down</div>
+                    <div class="fontclass">.naf-icon-arrowdown</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-arrowleft"></i>
+                    <div class="name">arrow-left</div>
+                    <div class="fontclass">.naf-icon-arrowleft</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-arrowright"></i>
+                    <div class="name">arrow-right</div>
+                    <div class="fontclass">.naf-icon-arrowright</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-arrowup"></i>
+                    <div class="name">arrow-up</div>
+                    <div class="fontclass">.naf-icon-arrowup</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-favorite"></i>
+                    <div class="name">favorite</div>
+                    <div class="fontclass">.naf-icon-favorite</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-good"></i>
+                    <div class="name">good</div>
+                    <div class="fontclass">.naf-icon-good</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-close"></i>
+                    <div class="name">close</div>
+                    <div class="fontclass">.naf-icon-close</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-refresh"></i>
+                    <div class="name">refresh</div>
+                    <div class="fontclass">.naf-icon-refresh</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-map"></i>
+                    <div class="name">map</div>
+                    <div class="fontclass">.naf-icon-map</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-account"></i>
+                    <div class="name">account</div>
+                    <div class="fontclass">.naf-icon-account</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-search"></i>
+                    <div class="name">search</div>
+                    <div class="fontclass">.naf-icon-search</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-download"></i>
+                    <div class="name">download</div>
+                    <div class="fontclass">.naf-icon-download</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-compass"></i>
+                    <div class="name">compass</div>
+                    <div class="fontclass">.naf-icon-compass</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-comments"></i>
+                    <div class="name">comments</div>
+                    <div class="fontclass">.naf-icon-comments</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-down"></i>
+                    <div class="name">down</div>
+                    <div class="fontclass">.naf-icon-down</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-up"></i>
+                    <div class="name">up</div>
+                    <div class="fontclass">.naf-icon-up</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-play"></i>
+                    <div class="name">play</div>
+                    <div class="fontclass">.naf-icon-play</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-error"></i>
+                    <div class="name">error</div>
+                    <div class="fontclass">.naf-icon-error</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-prompt"></i>
+                    <div class="name">prompt</div>
+                    <div class="fontclass">.naf-icon-prompt</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-success"></i>
+                    <div class="name">success</div>
+                    <div class="fontclass">.naf-icon-success</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-stop"></i>
+                    <div class="name">stop</div>
+                    <div class="fontclass">.naf-icon-stop</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-help"></i>
+                    <div class="name">help</div>
+                    <div class="fontclass">.naf-icon-help</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-time"></i>
+                    <div class="name">time</div>
+                    <div class="fontclass">.naf-icon-time</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-cry"></i>
+                    <div class="name">cry</div>
+                    <div class="fontclass">.naf-icon-cry</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-add"></i>
+                    <div class="name">add</div>
+                    <div class="fontclass">.naf-icon-add</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-minus"></i>
+                    <div class="name">minus</div>
+                    <div class="fontclass">.naf-icon-minus</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-link"></i>
+                    <div class="name">link</div>
+                    <div class="fontclass">.naf-icon-link</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-atm"></i>
+                    <div class="name">atm</div>
+                    <div class="fontclass">.naf-icon-atm</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-skip"></i>
+                    <div class="name">skip</div>
+                    <div class="fontclass">.naf-icon-skip</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-attachment"></i>
+                    <div class="name">attachment</div>
+                    <div class="fontclass">.naf-icon-attachment</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-browse"></i>
+                    <div class="name">browse</div>
+                    <div class="fontclass">.naf-icon-browse</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-phone"></i>
+                    <div class="name">phone</div>
+                    <div class="fontclass">.naf-icon-phone</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-bad"></i>
+                    <div class="name">bad</div>
+                    <div class="fontclass">.naf-icon-bad</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-text"></i>
+                    <div class="name">text</div>
+                    <div class="fontclass">.naf-icon-text</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-corpuser"></i>
+                    <div class="name">企业用户</div>
+                    <div class="fontclass">.naf-icon-corpuser</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-erweima1688"></i>
+                    <div class="name">二维码</div>
+                    <div class="fontclass">.naf-icon-erweima1688</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-quit"></i>
+                    <div class="name">quit</div>
+                    <div class="fontclass">.naf-icon-quit</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-folder1"></i>
+                    <div class="name">folder</div>
+                    <div class="fontclass">.naf-icon-folder1</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-dept"></i>
+                    <div class="name">部门</div>
+                    <div class="fontclass">.naf-icon-dept</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-report"></i>
+                    <div class="name">报表</div>
+                    <div class="fontclass">.naf-icon-report</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-shenhe"></i>
+                    <div class="name">审核</div>
+                    <div class="fontclass">.naf-icon-shenhe</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-info"></i>
+                    <div class="name">企业信息</div>
+                    <div class="fontclass">.naf-icon-info</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-corp"></i>
+                    <div class="name">企业</div>
+                    <div class="fontclass">.naf-icon-corp</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-doc"></i>
+                    <div class="name">公文</div>
+                    <div class="fontclass">.naf-icon-doc</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-log"></i>
+                    <div class="name">日志</div>
+                    <div class="fontclass">.naf-icon-log</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-verifycode"></i>
+                    <div class="name">验证码</div>
+                    <div class="fontclass">.naf-icon-verifycode</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-tag"></i>
+                    <div class="name">标签</div>
+                    <div class="fontclass">.naf-icon-tag</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-caogao"></i>
+                    <div class="name">草稿</div>
+                    <div class="fontclass">.naf-icon-caogao</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-dian"></i>
+                    <div class="name"></div>
+                    <div class="fontclass">.naf-icon-dian</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-doc2"></i>
+                    <div class="name">发文</div>
+                    <div class="fontclass">.naf-icon-doc2</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-fold"></i>
+                    <div class="name">fold</div>
+                    <div class="fontclass">.naf-icon-fold</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-unfold"></i>
+                    <div class="name">unfold</div>
+                    <div class="fontclass">.naf-icon-unfold</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-news"></i>
+                    <div class="name">信息管理</div>
+                    <div class="fontclass">.naf-icon-news</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-reply"></i>
+                    <div class="name">回复</div>
+                    <div class="fontclass">.naf-icon-reply</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-column"></i>
+                    <div class="name">栏目管理</div>
+                    <div class="fontclass">.naf-icon-column</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-dict"></i>
+                    <div class="name">字典</div>
+                    <div class="fontclass">.naf-icon-dict</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-system"></i>
+                    <div class="name">系统管理</div>
+                    <div class="fontclass">.naf-icon-system</div>
+                </li>
+            
+                <li>
+                <i class="icon naf-icons naf-icon-menu"></i>
+                    <div class="name">菜单</div>
+                    <div class="fontclass">.naf-icon-menu</div>
+                </li>
+            
+        </ul>
+
+        <h2 id="font-class-">font-class引用</h2>
+        <hr>
+
+        <p>font-class是unicode使用方式的一种变种,主要是解决unicode书写不直观,语意不明确的问题。</p>
+        <p>与unicode使用方式相比,具有如下特点:</p>
+        <ul>
+        <li>兼容性良好,支持ie8+,及所有现代浏览器。</li>
+        <li>相比于unicode语意明确,书写更直观。可以很容易分辨这个icon是什么。</li>
+        <li>因为使用class来定义图标,所以当要替换图标时,只需要修改class里面的unicode引用。</li>
+        <li>不过因为本质上还是使用的字体,所以多色图标还是不支持的。</li>
+        </ul>
+        <p>使用步骤如下:</p>
+        <h3 id="-fontclass-">第一步:引入项目下面生成的fontclass代码:</h3>
+
+
+        <pre><code class="lang-js hljs javascript"><span class="hljs-comment">&lt;link rel="stylesheet" type="text/css" href="./iconfont.css"&gt;</span></code></pre>
+        <h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
+        <pre><code class="lang-css hljs">&lt;<span class="hljs-selector-tag">i</span> <span class="hljs-selector-tag">class</span>="<span class="hljs-selector-tag">naf-icons</span> <span class="hljs-selector-tag">naf-icon-xxx</span>"&gt;&lt;/<span class="hljs-selector-tag">i</span>&gt;</code></pre>
+        <blockquote>
+        <p>"naf-icons"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。</p>
+        </blockquote>
+    </div>
+</body>
+</html>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 3252 - 0
admin-frame/public/naf-icons/demo_index.html


+ 0 - 0
admin-frame/public/naf-icons/demo_symbol.html


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels