asd123a20 há 3 anos atrás
pai
commit
4fb724ae52
70 ficheiros alterados com 17982 adições e 0 exclusões
  1. 3 0
      .browserslistrc
  2. 5 0
      .editorconfig
  3. 11 0
      .env
  4. 17 0
      .eslintrc.js
  5. 23 0
      .gitignore
  6. 5 0
      babel.config.js
  7. 64 0
      lib/plug/axios.js
  8. 15 0
      lib/plug/deepTree.js
  9. 9 0
      lib/plug/dict.js
  10. 68 0
      lib/style/index.less
  11. 69 0
      naf/data/deep-tree.vue
  12. 24 0
      naf/data/dialog -drawer.md
  13. 68 0
      naf/data/dialog -drawer.vue
  14. 166 0
      naf/data/editoritem.vue
  15. 19 0
      naf/data/form.md
  16. 80 0
      naf/data/form.vue
  17. 56 0
      naf/data/tables/grid.md
  18. 78 0
      naf/data/tables/naf-grid.vue
  19. 54 0
      naf/data/tables/pagination .vue
  20. 70 0
      naf/data/tables/search.vue
  21. 79 0
      naf/data/tables/table.vue
  22. 54 0
      naf/layout/breadcrumb.vue
  23. 34 0
      naf/layout/menu-item.vue
  24. 69 0
      naf/layout/menu.vue
  25. 103 0
      naf/layout/user.vue
  26. 12854 0
      package-lock.json
  27. 37 0
      package.json
  28. BIN
      public/favicon.ico
  29. 17 0
      public/index.html
  30. 7 0
      src/App.vue
  31. BIN
      src/assets/bg2.jpg
  32. BIN
      src/assets/home.png
  33. 1 0
      src/assets/logo1.svg
  34. 60 0
      src/components/HelloWorld.vue
  35. 18 0
      src/main.js
  36. 22 0
      src/router/gaf.js
  37. 45 0
      src/router/index.js
  38. 36 0
      src/router/wokes.js
  39. 50 0
      src/store/gaf/adminuser.js
  40. 35 0
      src/store/gaf/log.js
  41. 25 0
      src/store/gaf/login.js
  42. 43 0
      src/store/gaf/menu.js
  43. 50 0
      src/store/gaf/role.js
  44. 64 0
      src/store/index.js
  45. 60 0
      src/store/wokes/banner.js
  46. 60 0
      src/store/wokes/content.js
  47. 51 0
      src/store/wokes/files.js
  48. 50 0
      src/store/wokes/links.js
  49. 50 0
      src/store/wokes/menu.js
  50. 60 0
      src/store/wokes/pages.js
  51. 41 0
      src/store/wokes/webconfig.js
  52. 48 0
      src/views/frame/Home.vue
  53. 130 0
      src/views/frame/Login.vue
  54. 64 0
      src/views/frame/demo.vue
  55. 121 0
      src/views/frame/frame.vue
  56. 142 0
      src/views/frame/router-frame.vue
  57. 56 0
      src/views/gaf/log.vue
  58. 142 0
      src/views/gaf/role.vue
  59. 225 0
      src/views/gaf/rolepower.vue
  60. 164 0
      src/views/gaf/user.vue
  61. 221 0
      src/views/gaf/userpower.vue
  62. 298 0
      src/views/wokes/configuration/banner.vue
  63. 205 0
      src/views/wokes/configuration/links.vue
  64. 113 0
      src/views/wokes/configuration/webconfig.vue
  65. 392 0
      src/views/wokes/content/content.vue
  66. 93 0
      src/views/wokes/content/files.vue
  67. 198 0
      src/views/wokes/content/menu.vue
  68. 266 0
      src/views/wokes/content/pages.vue
  69. 93 0
      src/views/wokes/content/resource.vue
  70. 32 0
      vue.config.js

+ 3 - 0
.browserslistrc

@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not dead

+ 5 - 0
.editorconfig

@@ -0,0 +1,5 @@
+[*.{js,jsx,ts,tsx,vue}]
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 11 - 0
.env

@@ -0,0 +1,11 @@
+VUE_APP_ROOT_URL=/admin/
+VUE_APP_HEIGHT=100vh
+VUE_APP_WIDTH=1300px
+VUE_APP_MENU_WIDTH=270PX
+VUE_APP_MENU_BACKGROUNDCOLOR=#083e96
+VUE_APP_MENU_TEXTCOLOR=#FFF
+VUE_APP_MENU_ACTIVETEXTCOLOR=#ffd04b
+VUE_APP_MENU_TITLE=经济纵横管理系统
+VUE_APP_HOME_TITLEE=经济纵横管理系统
+VUE_APP_HOME_DESCRIPTION=基于内容管理、系统管理、网站配置等功能的优秀期刊网站管理系统。
+VUE_APP_TABS=false

+ 17 - 0
.eslintrc.js

@@ -0,0 +1,17 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true
+  },
+  extends: [
+    'plugin:vue/essential',
+    '@vue/standard'
+  ],
+  parserOptions: {
+    parser: 'babel-eslint'
+  },
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
+  }
+}

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 5 - 0
babel.config.js

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

+ 64 - 0
lib/plug/axios.js

@@ -0,0 +1,64 @@
+/* eslint-disable no-const-assign */
+import axios from 'axios'
+import router from '../../src/router/index'
+import { Message, Loading } from 'element-ui'
+// 构建axios实例
+axios.create({
+  baseURL: process.env.BASE_API,
+  timeout: 10000
+})
+let loadingInstance = null
+axios.interceptors.request.use(config => {
+  const dom = document.getElementById('content')
+  if (loadingInstance == null) loadingInstance = Loading.service({ target: dom })
+  const token = sessionStorage.getItem('token')
+  if (token) {
+    config.headers.Authorization = 'Bearer ' + token
+  }
+  return config
+},
+err => {
+  return Promise.reject(err)
+})
+axios.interceptors.response.use(
+  response => {
+    if (loadingInstance !== null) {
+      loadingInstance.close()
+      loadingInstance = null
+    }
+    if (response.data.errcode !== 0 || response.data.errcode === 403) {
+      if (response.data.errmsg.cmd) {
+        Message.error(response.data.errmsg.cmd)
+        return false
+      }
+      Message.error(response.data.errmsg)
+      return false
+    }
+    return response.data
+  },
+  error => {
+    const { status, data } = error.response
+    if (loadingInstance !== null) {
+      loadingInstance.close()
+      loadingInstance = null
+    }
+    if (status === 401) {
+      Message.error('请重新登录')
+      router.push('/login')
+      return false
+    }
+    if (status === 500) {
+      if (data.cmd) {
+        Message.error(data.cmd)
+        return false
+      }
+      if (data.errmsg) {
+        Message.error(data.errmsg)
+        return false
+      }
+    }
+    Message.error(data.errmsg)
+    return data
+  }
+)
+export default axios

+ 15 - 0
lib/plug/deepTree.js

@@ -0,0 +1,15 @@
+const deepTree = {
+  install (vue) {
+    vue.prototype.$deepTree = menus => {
+      if (!menus || menus.length <= 0) return
+      let root = menus.filter(p => p.parentId === '' || p.parentId === null)
+      const childrens = (item) => {
+        const children = menus.filter(p => item.id === p.parentId).map(p => childrens(p))
+        return { ...item, children }
+      }
+      root = root.map(p => childrens(p))
+      return root
+    }
+  }
+}
+export default deepTree

+ 9 - 0
lib/plug/dict.js

@@ -0,0 +1,9 @@
+const dict = {
+  install (vue) {
+    vue.prototype.$dict = function (item) {
+      const dict = this.$store.state.dict
+      return dict[item]
+    }
+  }
+}
+export default dict

+ 68 - 0
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;
+}

+ 69 - 0
naf/data/deep-tree.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="treeContainer">
+    <el-input size="mini" class="filter" v-if="treeFilter" placeholder="输入关键字进行搜索" v-model="filterText"></el-input>
+    <el-tree
+      :data="datas"
+      default-expand-all
+      :filter-node-method="filterNode"
+      :props="defaultProps"
+      @node-click="treeClick"
+      ref="deeptree"
+    >
+    </el-tree>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    // 形成树的扁平化数据  关联方式parentId
+    data: { type: Array },
+    // 是否启用搜索
+    treeFilter: { type: Boolean, default: false }
+  },
+  data () {
+    return {
+      filterText: '',
+      defaultProps: {
+        children: 'children',
+        label: 'name'
+      }
+    }
+  },
+  computed: {
+    datas () {
+      const data = this.$deepTree(this.data)
+      return data
+    }
+  },
+  methods: {
+    filterNode (value, data) {
+      if (!value) return true
+      return data.title.indexOf(value) !== -1
+    },
+    treeClick (data, node, event) {
+      console.log(data)
+      this.$emit('treeclick', data)
+    }
+  },
+  mounted () {},
+  watch: {
+    filterText (val) {
+      this.$refs.deeptree.filter(val)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.treeContainer {
+  width: 15%;
+  height: 100%;
+  border-right: 1px solid #dadada;
+  .filter {
+    width: 90%;
+    margin: 20px auto;
+    display: block;
+  }
+}
+</style>

+ 24 - 0
naf/data/dialog -drawer.md

@@ -0,0 +1,24 @@
+### title标题
+title: '这是标题'
+、、、
+### type选择弹出的类型 dialog = 弹窗, drawer = 抽屉(抽屉还是弹窗)默认弹窗
+type:'dialog'
+、、、
+### modal 点击这招是否关闭  默认关闭
+modal: true
+、、、
+### escape 退出键是否关闭  默认关闭
+escape: true
+、、、
+### close 是否显示退出按钮 默认显示
+close: true
+、、、
+### visible 弹窗控制  开启关闭 (必须) 默认开启
+visible: true || false
+、、、
+### width 弹窗宽度 弹窗默认 40% 抽屉默认 25%
+width: '40%'
+、、、
+### 传出事件  close  关闭事件
+@close="visible = false"
+、、、

+ 68 - 0
naf/data/dialog -drawer.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="ad-aw-box">
+    <el-drawer
+      v-if="type == 'drawer'"
+      :visible.sync="visible"
+      :wrapperClosable="modal"
+      :close-on-press-escape="escape"
+      :show-close="close"
+      :before-close="beforeClose"
+      :size="wd"
+    >
+      <template v-slot:title>
+        <h1>
+          {{ title }}
+        </h1>
+      </template>
+      <slot name="content"></slot>
+    </el-drawer>
+    <el-dialog
+      v-if="type == 'dialog'"
+      :title="title"
+      :close-on-click-modal="modal"
+      :close-on-press-escape="escape"
+      :show-close="close"
+      :visible.sync="visible"
+      :before-close="beforeClose"
+      :width="wd"
+    >
+      <slot name="content"></slot>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    // 标题
+    title: String,
+    type: { type: String, default: 'dialog' },
+    // 是否可以点击遮罩关闭
+    modal: { type: Boolean, default: true },
+    // 是否可以ESC关闭
+    escape: { type: Boolean, default: true },
+    // 是否显示退出按钮
+    close: { type: Boolean, default: true },
+    visible: { type: Boolean, default: true, required: true },
+    width: { type: String }
+  },
+  computed: {
+    wd () {
+      if (this.width) return this.width
+      const width = this.type === 'dialog' ? '40%' : '25%'
+      return width
+    }
+  },
+  data () {
+    return {}
+  },
+  methods: {
+    beforeClose (done) {
+      this.$emit('close')
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped></style>

+ 166 - 0
naf/data/editoritem.vue

@@ -0,0 +1,166 @@
+<template lang="html">
+  <div class="editor">
+    <div ref="toolbar" class="toolbar">
+    </div>
+    <div ref="editor" class="text">
+    </div>
+  </div>
+</template>
+
+<script>
+import E from 'wangeditor'
+import { createNamespacedHelpers } from 'vuex'
+const { mapActions } = createNamespacedHelpers('files')
+const token = sessionStorage.getItem('token')
+export default {
+  name: 'editoritem',
+  data () {
+    return {
+      // uploadPath,
+      editor: null,
+      info_: null
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  props: {
+    value: {
+      type: String,
+      default: ''
+    },
+    isClear: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    isClear (val) {
+      // 触发清除文本域内容
+      if (val) {
+        this.editor.txt.clear()
+        this.info_ = null
+      }
+    },
+    value: function (value) {
+      if (value !== this.editor.txt.html()) {
+        this.editor.txt.html(this.value)
+      }
+    }
+    // value为编辑框输入的内容,这里我监听了一下值,当父组件调用得时候,如果给value赋值了,子组件将会显示父组件赋给的值
+  },
+  mounted () {
+    this.seteditor()
+    this.editor.txt.html(this.value)
+  },
+  methods: {
+    ...mapActions(['filesupload']),
+    seteditor () {
+      const _this = this
+      // http://192.168.2.125:8080/admin/storage/create
+      this.editor = new E(this.$refs.toolbar, this.$refs.editor)
+      this.editor.config.uploadImgParams = { type: 'resource' }
+      this.editor.config.uploadImgShowBase64 = false // base 64 存储图片
+      this.editor.config.uploadImgServer = '/api/files/upload'// 配置服务器端地址
+      this.editor.config.uploadImgHeaders = { Authorization: `Bearer ${token}`, a: 100 }// 自定义 header
+      this.editor.config.uploadFileName = 'file' // 后端接受上传文件的参数名
+      this.editor.config.uploadImgMaxSize = 2 * 1024 * 1024 // 将图片大小限制为 2M
+      this.editor.config.uploadImgMaxLength = 6 // 限制一次最多上传 3 张图片
+      this.editor.config.uploadImgTimeout = 3 * 60 * 1000 // 设置超时时间
+      this.editor.config.showLinkImg = false
+      // 配置菜单
+      this.editor.config.menus = [
+        'head', // 标题
+        'bold', // 粗体
+        'fontSize', // 字号
+        'fontName', // 字体
+        'italic', // 斜体
+        'underline', // 下划线
+        'strikeThrough', // 删除线
+        'foreColor', // 文字颜色
+        'backColor', // 背景颜色
+        'link', // 插入链接
+        'list', // 列表
+        'justify', // 对齐方式
+        'quote', // 引用
+        'emoticon', // 表情
+        'image', // 插入图片
+        'table', // 表格
+        'video', // 插入视频
+        'code', // 插入代码
+        'undo', // 撤销
+        'redo', // 重复
+        'fullscreen' // 全屏
+      ]
+      this.editor.config.customUploadImg = async function (resultFiles, insertImgFn) {
+        // resultFiles 是 input 中选中的文件列表
+        var data = new FormData()
+        data.append('type', 'resource')
+        data.append('file', resultFiles[0])
+        const res = await _this.filesupload(data)
+        // insertImgFn 是获取图片 url 后,插入到编辑器的方法
+        const url = res.data.path
+        // 上传图片,返回结果,将图片插入到编辑器中
+        insertImgFn(url)
+      }
+
+      this.editor.config.uploadImgHooks = {
+        fail: (xhr, editor, result) => {
+          // 插入图片失败回调
+          console.log('插入图片失败回调')
+          console.log(xhr, editor, result)
+        },
+        success: (xhr, editor, result) => {
+          // 图片上传成功回调
+          console.log('图片上传成功回调')
+          console.log(xhr, editor, result)
+        },
+        timeout: (xhr, editor) => {
+          // 网络超时的回调
+        },
+        error: (xhr, editor) => {
+          // 图片上传错误的回调\
+          _this.$message.error('上传失败')
+        },
+        customInsert: (insertImg, result, editor) => {
+          console.log('图片上传成功,插入图片的回调')
+          console.log(result, editor)
+          // 图片上传成功,插入图片的回调
+          // result为上传图片成功的时候返回的数据,这里我打印了一下发现后台返回的是data:[{url:"路径的形式"},...]
+          // console.log(result.data[0].url)
+          // insertImg()为插入图片的函数
+          // 循环插入图片
+          // for (let i = 0; i < 1; i++) {
+          // console.log(result)
+          const url = result.data.path
+          insertImg(url)
+          // }
+        }
+      }
+      this.editor.config.onchange = (html) => {
+        this.info_ = html // 绑定当前逐渐地值
+        this.$emit('change', this.info_) // 将内容同步到父组件中
+      }
+      // 创建富文本编辑器
+      this.editor.create()
+    }
+  }
+}
+</script>
+
+<style lang="css">
+  .editor {
+    width: 100%;
+    margin: 0 auto;
+    position: relative;
+    z-index: 0;
+  }
+  .toolbar {
+    border: 1px solid #ccc;
+  }
+  .text {
+    border: 1px solid #ccc;
+    min-height: 300px;
+  }
+</style>

+ 19 - 0
naf/data/form.md

@@ -0,0 +1,19 @@
+### rules: 规则,对应字段需要的规则
+rules: {
+    name: [
+        { required: true, message: '请输入活动名称', trigger: 'blur' },
+        { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
+    ]
+}
+、、、
+### meta:设置标签的名称及字段
+meta: [
+    字段名         标题          字典(下拉选)     使用插槽
+    { name: 'xb', title: '性别', formatter: 'xb', slots: true }
+]
+、、、
+### data需要显示的数据(如对应字段存在值时传入,修改时使用)
+data: {
+    name: 'xxx'
+}
+、、、

+ 80 - 0
naf/data/form.vue

@@ -0,0 +1,80 @@
+<template>
+  <el-form :model="form" :rules="rules" ref="ruleForm" label-width="100px" size="mini">
+    <div v-for="(item, index) in meta" :key="index" >
+      <el-form-item :label="item.title" :prop="item.name" v-if="!item.slots || item.slots == 'field'">
+        <slot name="field" v-if="item.slots" v-bind="{ item, form }"></slot>
+        <el-input :size="item.size || 'mini'" v-if="!item.formatter && !item.slots && item.type !== 'number'" v-model="form[item.name]" :placeholder="item.placeholder || `请输入${item.title}`" :disabled="item.disabled"></el-input>
+        <el-input :size="item.size || 'mini'" v-if="!item.formatter && !item.slots && item.type == 'number'" v-model.number="form[item.name]" :placeholder="item.placeholder || `请输入${item.title}`" :disabled="item.disabled"></el-input>
+        <el-select :size="item.size || 'mini'" v-if="item.formatter && !item.slots" v-model="form[item.name]" :placeholder="item.placeholder || `请选择${item.title}`" :disabled="item.disabled">
+          <el-option v-for="(i, idx) in $dict(item.formatter)" :key="idx" :label="i.title" :value="i.value"></el-option>
+        </el-select>
+      </el-form-item>
+      <slot name="end" v-if="item.slots == 'end'" v-bind="{ item, form }"></slot>
+    </div>
+    <el-form-item>
+      <el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
+      <el-button v-if="isresetForm" @click="resetForm('ruleForm')">重置</el-button>
+      <el-button v-if="close" @click="$emit('close')">取消</el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+export default {
+  components: {},
+  props: {
+    rules: Object,
+    meta: Array,
+    data: Object,
+    isresetForm: {
+      type: Boolean,
+      default: false
+    },
+    close: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data () {
+    return {
+      form: {}
+    }
+  },
+  methods: {
+    reset () {
+      this.form = { ...this.data }
+    },
+    submitForm (formName) {
+      this.$refs[formName].validate((valid) => {
+        if (valid) {
+          this.meta.filter(p => {
+            if (p.formatter) {
+              const items = this.$dict(p.formatter)
+              const item = items.filter(z => z.title === this.form[p.name])
+              if (item.length > 0) this.form[p.name] = item[0].value
+            }
+          })
+          this.$emit('save', this.form)
+        } else {
+          console.log('error submit!!')
+          return false
+        }
+      })
+    },
+    formChage (name, val) {
+      this.$set(this.form, name, val)
+    },
+    resetForm (formName) {
+      this.$refs[formName].resetFields()
+    },
+    clearValidate (formName) {
+      this.$refs.ruleForm.clearValidate()
+    }
+  },
+  mounted () {
+    this.reset()
+  }
+}
+</script>
+
+<style lang="less" scoped></style>

+ 56 - 0
naf/data/tables/grid.md

@@ -0,0 +1,56 @@
+naf-grid  参数定义 (naf-grid为  搜索、 列表、 分页、的集合)
+、、、
+### search 是否启用搜索 默认启用
+:search="true"
+、、、
+### pagination 是否启用分页  默认启用
+:pagination="true"
+、、、
+### readonly 是否显示操作列 默认启用
+:readonly="true"
+、、、
+### selection 是否显示多选 默认启用
+:selection="true"
+、、、
+### operation 操作栏数组 默认修改删除
+operation: [
+    { name: 'edit', title: '编辑', icons: 'el-icon-edit' },
+    { name: 'delete', title: '删除', icons: 'el-icon-delete' }
+]
+name: 返回的方法名, title:按钮文字, icons:按钮图标
+、、、
+### meta 定义表格字段
+meta: [
+    { name: 'xb', title: '性别', formatter: 'xb', filter: true, width: '100' },
+]
+name: 字段名, title: 列表标题, formatter: 使用字典得类名, filter:是否使用该字段搜索, width: 该字段列表宽度(默认自适应)
+、、、
+### data列表展示数据 (类型为数组array)
+:data="[]"
+、、、
+### total分页总条数(类型为数字number)默认为0
+:total="100"
+、、、
+
+
+事件定义
+### 双击事件 dblclick  默认参数当前双击行数据
+dblclick(e) {
+    console.log(e)
+}
+、、、
+### 默认修改事件 edit 默认参数当前修改行数据 (事件名可被operation覆盖)
+edit(e) {
+    console.log(e)
+}
+、、、
+### 默认删除事件 delete 默认参数当前删除行数据 (事件名可被operation覆盖)
+delete(e) {
+    console.log(e)
+}
+、、、
+### query 查询事件 当页码条数改变时会调用该事件,当查询条件改变时会调用该事件  参数为{ filter, paging }  filter = 查询条件, paging = 页码/条数
+query({ filter, paging }) {
+    console.log(filter, paging)
+}
+、、、

+ 78 - 0
naf/data/tables/naf-grid.vue

@@ -0,0 +1,78 @@
+<template>
+  <div class="container">
+    <naf-search @handlefilter="handleFilter" :filterList="filterList" v-if="search">
+    </naf-search>
+    <naf-table :meta="meta" :selection="selection" @selection="$emit('selection', $event)" :operation="operation" :data="data" :readonly="readonly" @oper="handleOper"></naf-table>
+    <naf-pagination ref="pagination" @handlechange="handleChange" :total="total" v-if="pagination"></naf-pagination>
+  </div>
+</template>
+
+<script>
+import nafSearch from '@naf/data/tables/search'
+import nafTable from '@naf/data/tables/table'
+import nafPagination from '@naf/data/tables/pagination '
+export default {
+  components: {
+    nafSearch,
+    nafTable,
+    nafPagination
+  },
+  props: {
+    // 是否启用搜索
+    search: { type: Boolean, default: true },
+    // 是否启用分页
+    pagination: { type: Boolean, default: true },
+    // 是否显示操作列
+    readonly: { type: Boolean, default: true },
+    // 是否显示多选
+    selection: { type: Boolean, default: true },
+    // 操作栏数组
+    operation: {
+      default: () => [
+        { name: 'edit', title: '编辑', icons: 'el-icon-edit' },
+        { name: 'delete', title: '删除', icons: 'el-icon-delete' }
+      ]
+    },
+    // 表格字段参数
+    meta: { type: Array, required: true },
+    // 数据源
+    data: Array,
+    // 总条数
+    total: { type: Number, default: 0 }
+  },
+  data () {
+    return {
+      paging: {},
+      filter: {}
+    }
+  },
+  computed: {
+    filterList () {
+      return this.meta.filter(p => (p.filter && p.filter === true))
+    }
+  },
+  methods: {
+    resetpage (val) {
+      this.$refs.pagination.resetpage()
+    },
+    handleChange ({ page, size }) {
+      this.paging = { page, size }
+      this.query()
+    },
+    handleFilter (filter) {
+      this.filter = filter
+      this.paging.page = 0
+      this.query()
+    },
+    query () {
+      this.$emit('query', { filter: this.filter, paging: this.paging })
+    },
+    handleOper ({ event, data }) {
+      this.$emit(event, data)
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped></style>

+ 54 - 0
naf/data/tables/pagination .vue

@@ -0,0 +1,54 @@
+<template>
+ <el-pagination
+    class="pagination"
+    @size-change="handleSizeChange"
+    @current-change="handleCurrentChange"
+    :current-page="page"
+    :page-sizes="[10,20,50,100]"
+    :page-size="size"
+    layout="total, sizes, prev, pager, next, jumper"
+    :total="total">
+  </el-pagination>
+</template>
+
+<script>
+export default {
+  props: {
+    total: { type: Number, default: 0 }
+  },
+  data () {
+    return {
+      page: 0,
+      size: 10
+    }
+  },
+  methods: {
+    resetpage (val) {
+      if (val < 0) {
+        this.page = 0
+      } else {
+        this.page = val
+      }
+    },
+    handleSizeChange (val) {
+      this.size = val
+      this.handleChange()
+    },
+    handleCurrentChange (val) {
+      this.page = val
+      this.handleChange()
+    },
+    handleChange () {
+      this.$emit('handlechange', { page: this.page, size: this.size })
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped>
+.pagination {
+  margin-left: 1%;
+  margin-top: 20px;
+}
+</style>

+ 70 - 0
naf/data/tables/search.vue

@@ -0,0 +1,70 @@
+<template>
+  <el-form :inline="true" :model="form" class="demo-form-inline" size="mini">
+    <el-form-item v-for="(item, index) in simpleFields" :key="index" :label="item.title">
+      <el-select class="prepend" v-if="item.formatter" v-model="form[item.name]" :placeholder="item.placeholder || '请选择'">
+        <el-option
+          v-for="i in $dict(item.formatter)"
+          :key="i.value"
+          :label="i.title"
+          :value="i.value">
+        </el-option>
+      </el-select>
+      <el-input v-else v-model="form[item.name]" :placeholder="item.placeholder || ''"></el-input>
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="onSubmit">查询</el-button>
+      <el-button type="primary" @click="reset">重置</el-button>
+      <el-button type="primary" v-if="filterList.length > 4" @click="more = !more">{{ !more ? '更多' : '收起' }}</el-button>
+    </el-form-item>
+    <slot v-if="more">
+      <el-form :inline="true" :model="form" size="mini">
+        <el-form-item v-for="(item, index) in moreFields" :key="index" :label="item.title">
+          <el-input v-model="form[item.name]" :placeholder="item.placeholder || ''"></el-input>
+        </el-form-item>
+      </el-form>
+    </slot>
+  </el-form>
+</template>
+
+<script>
+export default {
+  props: {
+    filterList: Array,
+    maxFields: { type: Number, default: 4 }
+  },
+  computed: {
+    simpleFields () {
+      return this.filterList.slice(0, this.maxFields)
+    },
+    moreFields () {
+      return this.filterList.slice(this.maxFields) || []
+    }
+  },
+  data () {
+    return {
+      form: {},
+      more: false
+    }
+  },
+  methods: {
+    onSubmit () {
+      this.$emit('handlefilter', this.form)
+    },
+    reset () {
+      this.form = {}
+      this.$emit('handlefilter', this.form)
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped>
+.demo-form-inline {
+  margin-left: 1%;
+  margin-top: 10px;
+}
+.prepend.el-select {
+  width: 100px;
+}
+</style>

+ 79 - 0
naf/data/tables/table.vue

@@ -0,0 +1,79 @@
+<template>
+  <el-table size="mini" :data="datas" class="table" @selection-change="handleSelectionChange" @row-dblclick="$emit('oper', { event: 'dblclick', data: $event })" border>
+    <el-table-column v-if="selection" type="selection" width="55"></el-table-column>
+    <el-table-column v-for="(item, index) in meta" :key="index" :prop="item.name" :label="item.title" :width="item.width || ''" show-overflow-tooltip></el-table-column>
+    <el-table-column label="操作" v-if="readonly">
+      <template slot-scope="scope">
+        <el-button size="mini" type="text" v-for="(item, index) in operation" :key="index" @click="oper(item, scope)">
+          <i v-if="item.icons" :class="item.icons"></i>
+          {{ item.title }}
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+export default {
+  components: {},
+  props: {
+    data: { type: Array, defalut: [] },
+    meta: Array,
+    operation: Array,
+    selection: Boolean,
+    readonly: Boolean
+  },
+  computed: {
+    datas () {
+      const formatter = this.meta.filter(p => p.formatter)
+      const data = [...this.data]
+      formatter.filter(i => {
+        data.map(p => {
+          const dict = this.$dict(i.name) || []
+          // eslint-disable-next-line eqeqeq
+          const item = dict.filter(z => p[i.name] == z.value)
+          if (item.length > 0) {
+            p[i.name] = item[0].title
+          }
+        })
+      })
+      return data
+    }
+  },
+  data () {
+    return {}
+  },
+  methods: {
+    handleSelectionChange (val) {
+      this.$emit('selection', val)
+    },
+    oper (item, scope) {
+      const formatter = this.meta.filter(p => p.formatter)
+      const data = { ...scope.row }
+      formatter.filter(i => {
+        for (const key in data) {
+          const dict = this.$dict(key) || []
+          if (dict.length > 0) {
+            // eslint-disable-next-line eqeqeq
+            const item = dict.filter(z => data[key] == z.title)
+            if (item.length > 0) {
+              data[key] = item[0].value
+            }
+          }
+        }
+      })
+      this.$emit('oper', { event: item.name, data: data })
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped>
+.table {
+  width: 98%;
+  margin: 0 auto;
+  max-height: 60vh;
+  overflow-y: auto;
+}
+</style>

+ 54 - 0
naf/layout/breadcrumb.vue

@@ -0,0 +1,54 @@
+<template>
+  <el-breadcrumb class="breadcrumb" separator="/">
+    <el-breadcrumb-item :to="{ path: '/frame' }">首页</el-breadcrumb-item>
+    <el-breadcrumb-item v-for="(item, index) in list" :key="index">{{ item.title }}</el-breadcrumb-item>
+    </el-breadcrumb>
+</template>
+
+<script>
+export default {
+  props: {
+    menuItems: Array
+  },
+  data () {
+    return {
+      routers: []
+    }
+  },
+  computed: {
+    list () {
+      const path = this.$route.path
+      if (path === '/frame') return false
+      const item = this.items(path)
+      return item
+    }
+  },
+  methods: {
+    items (path) {
+      const menuList = []
+      const nemus = (path) => {
+        this.menuItems.filter(p => {
+          if (`/frame${p.path}` === path) {
+            if (p.parentId !== null && p.parentId !== '') {
+              const item = this.menuItems.filter(i => i.id === p.parentId)
+              nemus(`/frame${item[0].path}`)
+            }
+            menuList.push(p)
+          }
+        })
+      }
+      nemus(path)
+      return menuList
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped>
+.breadcrumb {
+  line-height: 2em;
+  text-indent: 0.5em;
+  border-bottom: 1px solid #e6e6e6;
+}
+</style>

+ 34 - 0
naf/layout/menu-item.vue

@@ -0,0 +1,34 @@
+<template>
+  <el-submenu :index="index" v-if="item.children && item.children.length > 0">
+    <template slot="title">
+      <i :class="item.icons"></i>
+      <span slot="title">{{ item.title }}</span>
+    </template>
+    <naf-menu-item v-for="(item, idx) in item.children" :key="idx" :index="item.path" :item="item" @naf-menu-item="$emit('naf-menu-item', $event)"></naf-menu-item>
+  </el-submenu>
+  <el-menu-item :index="index" @click="$emit('naf-menu-item', item)" v-else>
+    <i :class="item.icons"></i>
+    <span slot="title" v-if="item.title.length < 9">{{ item.title }}</span>
+    <el-tooltip slot="title" v-else :content="item.title" placement="top" effect="light">
+      <span>{{ item.title.substr(0, 9) + '...' }}</span>
+    </el-tooltip>
+  </el-menu-item>
+</template>
+
+<script>
+export default {
+  name: 'naf-menu-item',
+  props: {
+    item: Object,
+    index: String
+  },
+  components: {},
+  data () {
+    return {}
+  },
+  methods: {},
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped></style>

+ 69 - 0
naf/layout/menu.vue

@@ -0,0 +1,69 @@
+<template>
+  <el-menu :default-active="active" v-bind="config" class="el-menu-vertical-demo" :collapse="isCollapse">
+    <naf-menu-item @naf-menu-item="menuItem" v-for="(item, index) in menuItems" :key="index" :item="item" :index="item.path"></naf-menu-item>
+  </el-menu>
+</template>
+
+<script>
+import nafMenuItem from './menu-item'
+const config = {
+  backgroundColor: process.env.VUE_APP_MENU_BACKGROUNDCOLOR,
+  textColor: process.env.VUE_APP_MENU_TEXTCOLOR,
+  activeTextColor: process.env.VUE_APP_MENU_ACTIVETEXTCOLOR
+}
+export default {
+  props: {
+    // 菜单折叠
+    isCollapse: { type: Boolean, default: false },
+    // 树形结构菜单数据
+    menuItems: Array
+  },
+  components: {
+    nafMenuItem
+  },
+  computed: {
+    // 按钮选中状态计算树形
+    active () {
+      // 如果是首页返回空
+      if (this.$route.path === '/frame') return ''
+      // 定义当前选中的菜单
+      let active = ''
+      // 自定义函数  参数是当前路由与需要过滤的数组
+      const item = (path, items) => {
+        // 数组过滤
+        items.filter(p => {
+          // 如果数组地址等于传入地址 给当前项赋值
+          if (`/frame${p.path}` === path) active = p.path
+          // 如果存在子级数组 递归调用
+          if (p.children) item(path, p.children)
+        })
+      }
+      // 当前路由
+      const path = this.$route.path
+      // 调用自定义函数
+      item(path, this.menuItems)
+      return active
+    }
+  },
+  data () {
+    return {
+      config
+    }
+  },
+  methods: {
+    // 菜单点击跳转地址
+    menuItem (e) {
+      const url = `/frame${e.path}`
+      if (url === this.$route.path) return
+      this.$router.push(url)
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped>
+.el-menu-vertical-demo {
+  height: 100%;
+}
+</style>

+ 103 - 0
naf/layout/user.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="container">
+    <el-avatar class="avatar" :size="40" :src="circleUrl" :icon="circleUrl == null ? 'el-icon-user-solid' : ''"></el-avatar>
+    <el-dropdown @command="handleCommand">
+        <span class="el-dropdown-link">
+            {{ userName }}
+        </span>
+        <el-dropdown-menu slot="dropdown">
+            <!-- <el-dropdown-item name="pwd">修改密码</el-dropdown-item> -->
+            <!-- <el-dropdown-item>系统任务</el-dropdown-item>
+            <el-dropdown-item>系统消息</el-dropdown-item> -->
+            <el-dropdown-item command="clocs">退出系统</el-dropdown-item>
+        </el-dropdown-menu>
+    </el-dropdown>
+    <dialog-drawer type="dialog" :visible="visible" title="修改密码" @close="visible = false">
+      <template v-slot:content>
+        <el-form ref="form" :rules="rules" :model="form" label-width="80px">
+          <el-form-item label="原密码" prop="password">
+            <el-input v-model="form.password"></el-input>
+          </el-form-item>
+          <el-form-item label="新密码" prop="newpassword">
+            <el-input v-model="form.newpassword"></el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="onSubmit">提交</el-button>
+          </el-form-item>
+        </el-form>
+      </template>
+    </dialog-drawer>
+  </div>
+</template>
+
+<script>
+import dialogDrawer from '@naf/data/dialog -drawer'
+import { mapActions } from 'vuex'
+export default {
+  components: {
+    dialogDrawer
+  },
+  data () {
+    return {
+      menuitem: '',
+      circleUrl: null,
+      form: {},
+      visible: false,
+      rules: {
+        password: [
+          { required: true, message: '请输入原密码', trigger: 'blur' }
+        ],
+        newpassword: [
+          { required: true, message: '请输入新密码', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  computed: {
+    userName () {
+      const userName = sessionStorage.getItem('userName') || ''
+      return userName
+    }
+  },
+  methods: {
+    ...mapActions(['editPwa']),
+    handleCommand (command) {
+      console.log(command)
+      if (command === 'pwd') this.visible = true
+      if (command === 'clocs') {
+        localStorage.removeItem('itemId')
+        this.$router.push('/login')
+      }
+    },
+    async onSubmit () {
+      const userName = sessionStorage.getItem('userName')
+      const res = await this.editPwa({ ...this.form, userName })
+      if (res.errcode === 0) {
+        this.$message({
+          message: '修改成功',
+          type: 'success'
+        })
+        sessionStorage.clear()
+      } else {
+        this.$message.error(res.errmsg)
+      }
+      this.visible = false
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  display: flex;
+  .avatar {
+    margin-top: 8%;
+    margin-right: 5%;
+  }
+  .el-dropdown-link{
+    color: #fff;
+    line-height: 4em;
+  }
+}
+</style>

Diff do ficheiro suprimidas por serem muito extensas
+ 12854 - 0
package-lock.json


+ 37 - 0
package.json

@@ -0,0 +1,37 @@
+{
+  "name": "cms-web",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "axios": "^0.21.1",
+    "core-js": "^3.6.5",
+    "element-ui": "^2.15.3",
+    "vue": "^2.6.11",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0",
+    "wangeditor": "^4.7.5"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-plugin-router": "~4.5.0",
+    "@vue/cli-plugin-vuex": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/eslint-config-standard": "^5.1.2",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-import": "^2.20.2",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-promise": "^4.2.1",
+    "eslint-plugin-standard": "^4.0.0",
+    "eslint-plugin-vue": "^6.2.2",
+    "less": "^3.0.4",
+    "less-loader": "^5.0.0",
+    "vue-template-compiler": "^2.6.11"
+  }
+}

BIN
public/favicon.ico


+ 17 - 0
public/index.html

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

+ 7 - 0
src/App.vue

@@ -0,0 +1,7 @@
+<template>
+  <div id="app">
+    <router-view/>
+  </div>
+</template>
+
+<style lang="less"></style>

BIN
src/assets/bg2.jpg


BIN
src/assets/home.png


Diff do ficheiro suprimidas por serem muito extensas
+ 1 - 0
src/assets/logo1.svg


+ 60 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="hello">
+    <h1>{{ msg }}</h1>
+    <p>
+      For a guide and recipes on how to configure / customize this project,<br>
+      check out the
+      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
+    </p>
+    <h3>Installed CLI Plugins</h3>
+    <ul>
+      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
+      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
+      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
+      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
+    </ul>
+    <h3>Essential Links</h3>
+    <ul>
+      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
+      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
+      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
+      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
+      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
+    </ul>
+    <h3>Ecosystem</h3>
+    <ul>
+      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
+      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
+      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
+      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
+      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
+    </ul>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HelloWorld',
+  props: {
+    msg: String
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped lang="less">
+h3 {
+  margin: 40px 0 0;
+}
+ul {
+  list-style-type: none;
+  padding: 0;
+}
+li {
+  display: inline-block;
+  margin: 0 10px;
+}
+a {
+  color: #42b983;
+}
+</style>

+ 18 - 0
src/main.js

@@ -0,0 +1,18 @@
+import Vue from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+import ElementUI from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css'
+import deepTree from '@lib/plug/deepTree'
+import dict from '@lib/plug/dict'
+import '@lib/style/index.less'
+Vue.config.productionTip = false
+Vue.use(ElementUI)
+Vue.use(deepTree)
+Vue.use(dict)
+new Vue({
+  router,
+  store,
+  render: h => h(App)
+}).$mount('#app')

+ 22 - 0
src/router/gaf.js

@@ -0,0 +1,22 @@
+export default [
+  {
+    path: '/frame/gaf/user',
+    component: () => import('../views/gaf/user.vue')
+  },
+  {
+    path: '/frame/gaf/role',
+    component: () => import('../views/gaf/role.vue')
+  },
+  {
+    path: '/frame/gaf/userpower',
+    component: () => import('../views/gaf/userpower.vue')
+  },
+  {
+    path: '/frame/gaf/rolepower',
+    component: () => import('../views/gaf/rolepower.vue')
+  },
+  {
+    path: '/frame/gaf/log',
+    component: () => import('../views/gaf/log.vue')
+  }
+]

+ 45 - 0
src/router/index.js

@@ -0,0 +1,45 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+import gaf from './gaf'
+import wokes from './wokes'
+Vue.use(VueRouter)
+const routes = [
+  {
+    path: '/',
+    redirect: '/frame'
+  },
+  {
+    path: '/login',
+    component: () => import('../views/frame/Login.vue')
+  },
+  {
+    path: '/frame',
+    component: () => import('../views/frame/frame.vue'),
+    children: [
+      {
+        path: '/',
+        component: () => import('../views/frame/Home.vue')
+      },
+      // gaf
+      ...gaf,
+      // wokes
+      ...wokes
+    ]
+  }
+]
+const router = new VueRouter({
+  mode: 'history',
+  base: process.env.BASE_URL,
+  routes
+})
+router.beforeEach((to, from, next) => {
+  if (to.path === from.path) return
+  // 此处判断登录状态  失效则退回登录页  清空登录状态
+  const token = sessionStorage.getItem('token')
+  if (!token && !/\/login$/.test(to.path)) {
+    next('/login')
+  }
+  next()
+})
+
+export default router

+ 36 - 0
src/router/wokes.js

@@ -0,0 +1,36 @@
+export default [
+  // 以下是系统配置
+  {
+    path: '/frame/wokes/banner',
+    component: () => import('../views/wokes/configuration/banner.vue')
+  },
+  {
+    path: '/frame/wokes/links',
+    component: () => import('../views/wokes/configuration/links.vue')
+  },
+  {
+    path: '/frame/wokes/webconfig',
+    component: () => import('../views/wokes/configuration/webconfig.vue')
+  },
+  // 以下是内容管理
+  {
+    path: '/frame/wokes/menu',
+    component: () => import('../views/wokes/content/menu.vue')
+  },
+  {
+    path: '/frame/wokes/content',
+    component: () => import('../views/wokes/content/content.vue')
+  },
+  {
+    path: '/frame/wokes/pages',
+    component: () => import('../views/wokes/content/pages.vue')
+  },
+  {
+    path: '/frame/wokes/files',
+    component: () => import('../views/wokes/content/files.vue')
+  },
+  {
+    path: '/frame/wokes/resource',
+    component: () => import('../views/wokes/content/resource.vue')
+  }
+]

+ 50 - 0
src/store/gaf/adminuser.js

@@ -0,0 +1,50 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  adminUser: '/api/adminUser/query',
+  usercreate: '/api/adminUser/create',
+  userupdate: '/api/adminUser/update',
+  userdelete: '/api/adminUser/delete/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  userList: [],
+  total: 0
+})
+
+// actions
+const actions = {
+  async getUser ({ commit }, { filter, paging } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.adminUser, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('adminUser', res)
+    return res
+  },
+  async usercreate ({ commit }, payload) {
+    const res = await axios.post(api.usercreate, payload)
+    return res
+  },
+  async userupdate ({ commit }, payload) {
+    const res = await axios.post(api.userupdate, payload)
+    return res
+  },
+  async userdelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.userdelete}${_id}`)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  adminUser (state, payload) {
+    state.userList = payload.data
+    state.total = payload.total
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 35 - 0
src/store/gaf/log.js

@@ -0,0 +1,35 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  log: '/api/log/query'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  logList: [],
+  total: 0
+})
+
+// actions
+const actions = {
+  async logquery ({ commit }, { filter, paging } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.log, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('log', res)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  log (state, payload) {
+    state.logList = payload.data
+    state.total = payload.total
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 25 - 0
src/store/gaf/login.js

@@ -0,0 +1,25 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  login: '/api/power/login'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({})
+
+// actions
+const actions = {
+  async login ({ commit }, payload) {
+    const res = await axios.post(api.login, payload)
+    return res
+  }
+}
+
+// mutations
+const mutations = {}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 43 - 0
src/store/gaf/menu.js

@@ -0,0 +1,43 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  menus: '/api/adminmenu/query',
+  getUserMenu: '/api/power/getUserMenu'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  menuList: [],
+  userMenuList: []
+})
+
+// actions
+const actions = {
+  async getmenu ({ commit }, payload) {
+    const res = await axios.get(api.menus)
+    if (res.errcode === 0) commit('menus', res.data)
+    return res
+  },
+  // 查询 用户 角色 菜单
+  async getusermenu ({ commit }, payload) {
+    const res = await axios.get(api.getUserMenu)
+    if (res.errcode === 0) commit('userMenu', res.data)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  menus (state, payload) {
+    state.menuList = payload
+  },
+  userMenu (state, payload) {
+    state.userMenuList = payload
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 50 - 0
src/store/gaf/role.js

@@ -0,0 +1,50 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  rolequery: '/api/role/query',
+  rolecreate: '/api/role/create',
+  roleupdate: '/api/role/update',
+  roledelete: '/api/role/delete/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  roleList: [],
+  total: 0
+})
+
+// actions
+const actions = {
+  async rolequery ({ commit }, { filter, paging } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.rolequery, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('role', res)
+    return res
+  },
+  async rolecreate ({ commit }, payload) {
+    const res = await axios.post(api.rolecreate, payload)
+    return res
+  },
+  async roleupdate ({ commit }, payload) {
+    const res = await axios.post(api.roleupdate, payload)
+    return res
+  },
+  async roledelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.roledelete}${_id}`)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  role (state, payload) {
+    state.roleList = payload.data
+    state.total = payload.total
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 64 - 0
src/store/index.js

@@ -0,0 +1,64 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+// 系统
+import menu from './gaf/menu'
+import adminuser from './gaf/adminuser'
+import login from './gaf/login'
+import role from './gaf/role'
+import log from './gaf/log'
+// 内容
+import wokesmenu from './wokes/menu'
+import webconfig from './wokes/webconfig'
+import pages from './wokes/pages'
+import content from './wokes/content'
+import files from './wokes/files'
+import links from './wokes/links'
+import banner from './wokes/banner'
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: {
+    dict: {
+      state: [
+        { value: '0', title: '正常' },
+        { value: '1', title: '停用' }
+      ],
+      type: [
+        { value: '0', title: '栏目' },
+        { value: '1', title: '链接' },
+        { value: '2', title: '单页' }
+        // { value: '3', title: '父级' }
+      ],
+      columns: [],
+      result: [
+        { value: '成功', title: '成功' },
+        { value: '失败', title: '失败' }
+      ]
+    }
+  },
+  mutations: {
+    setcolumns (state, payload) {
+      const list = []
+      payload.filter(p => {
+        list.push({ value: p.code, title: p.name })
+      })
+      state.dict.columns = list
+    }
+  },
+  actions: {
+  },
+  modules: {
+    menu,
+    adminuser,
+    login,
+    role,
+    log,
+    wokesmenu,
+    webconfig,
+    pages,
+    content,
+    files,
+    links,
+    banner
+  }
+})

+ 60 - 0
src/store/wokes/banner.js

@@ -0,0 +1,60 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  bannerquery: '/api/banner/query',
+  bannercreate: '/api/banner/create',
+  bannerupdate: '/api/banner/update',
+  bannerdelete: '/api/banner/delete/',
+  bannerdetails: '/api/banner/details/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  bannerList: [],
+  total: 0,
+  bannerItem: {}
+})
+
+// actions
+const actions = {
+  async bannerquery ({ commit }, { filter, paging } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.bannerquery, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('banner', res)
+    return res
+  },
+  async bannercreate ({ commit }, payload) {
+    const res = await axios.post(api.bannercreate, payload)
+    return res
+  },
+  async bannerupdate ({ commit }, payload) {
+    const res = await axios.post(api.bannerupdate, payload)
+    return res
+  },
+  async bannerdelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.bannerdelete}${_id}`)
+    return res
+  },
+  async bannerdetails ({ commit }, { _id }) {
+    const res = await axios.get(`${api.bannerdetails}${_id}`)
+    if (res.errcode === 0) commit('details', res)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  banner (state, payload) {
+    state.bannerList = payload.data
+    state.total = payload.total
+  },
+  details (state, payload) {
+    state.bannerItem = payload.data
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 60 - 0
src/store/wokes/content.js

@@ -0,0 +1,60 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  contentquery: '/api/content/query',
+  contentcreate: '/api/content/create',
+  contentupdate: '/api/content/update',
+  contentdelete: '/api/content/delete/',
+  contentdetails: '/api/content/details/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  contentList: [],
+  total: 0,
+  contentItem: {}
+})
+
+// actions
+const actions = {
+  async contentquery ({ commit }, { filter, paging } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.contentquery, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('content', res)
+    return res
+  },
+  async contentcreate ({ commit }, payload) {
+    const res = await axios.post(api.contentcreate, payload)
+    return res
+  },
+  async contentupdate ({ commit }, payload) {
+    const res = await axios.post(api.contentupdate, payload)
+    return res
+  },
+  async contentdelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.contentdelete}${_id}`)
+    return res
+  },
+  async contentdetails ({ commit }, { _id }) {
+    const res = await axios.get(`${api.contentdetails}${_id}`)
+    if (res.errcode === 0) commit('details', res)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  content (state, payload) {
+    state.contentList = payload.data
+    state.total = payload.total
+  },
+  details (state, payload) {
+    state.contentItem = payload.data
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 51 - 0
src/store/wokes/files.js

@@ -0,0 +1,51 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  filesupload: '/api/files/upload',
+  filesquery: '/api/files/query',
+  filesdelete: '/api/files/delete/',
+  filesdetails: '/api/files/details/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  filesList: [],
+  total: 0
+})
+
+// actions
+const actions = {
+  async filesupload ({ commit }, payload) {
+    const config = {
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    }
+    const res = await axios.post(api.filesupload, payload, config)
+    return res
+  },
+  async filesquery ({ commit }, { filter, paging } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.filesquery, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('files', res)
+    return res
+  },
+  async filesdelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.filesdelete}${_id}`)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  files (state, payload) {
+    state.filesList = payload.data
+    state.total = payload.total
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 50 - 0
src/store/wokes/links.js

@@ -0,0 +1,50 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  linksquery: '/api/links/query',
+  linkscreate: '/api/links/create',
+  linksupdate: '/api/links/update',
+  linksdelete: '/api/links/delete/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  linksList: [],
+  total: 0
+})
+
+// actions
+const actions = {
+  async linksquery ({ commit }, { filter, paging } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.linksquery, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('links', res)
+    return res
+  },
+  async linkscreate ({ commit }, payload) {
+    const res = await axios.post(api.linkscreate, payload)
+    return res
+  },
+  async linksupdate ({ commit }, payload) {
+    const res = await axios.post(api.linksupdate, payload)
+    return res
+  },
+  async linksdelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.linksdelete}${_id}`)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  links (state, payload) {
+    state.linksList = payload.data
+    state.total = payload.total
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 50 - 0
src/store/wokes/menu.js

@@ -0,0 +1,50 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  menuquery: '/api/menu/query',
+  menucreate: '/api/menu/create',
+  menuupdate: '/api/menu/update',
+  menudelete: '/api/menu/delete/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  menuList: [],
+  total: 0
+})
+
+// actions
+const actions = {
+  async menuquery ({ commit }, { filter = {}, paging = {} } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.menuquery, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('menu', res)
+    return res
+  },
+  async menucreate ({ commit }, payload) {
+    const res = await axios.post(api.menucreate, payload)
+    return res
+  },
+  async menuupdate ({ commit }, payload) {
+    const res = await axios.post(api.menuupdate, payload)
+    return res
+  },
+  async menudelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.menudelete}${_id}`)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  menu (state, payload) {
+    state.menuList = payload.data
+    state.total = payload.total
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 60 - 0
src/store/wokes/pages.js

@@ -0,0 +1,60 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  pagequery: '/api/page/query',
+  pagecreate: '/api/page/create',
+  pageupdate: '/api/page/update',
+  pagedelete: '/api/page/delete/',
+  pagedetails: '/api/page/details/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  pageList: [],
+  total: 0,
+  pageItem: {}
+})
+
+// actions
+const actions = {
+  async pagequery ({ commit }, { filter = {}, paging = {} } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.pagequery, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('page', res)
+    return res
+  },
+  async pagecreate ({ commit }, payload) {
+    const res = await axios.post(api.pagecreate, payload)
+    return res
+  },
+  async pageupdate ({ commit }, payload) {
+    const res = await axios.post(api.pageupdate, payload)
+    return res
+  },
+  async pagedelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.pagedelete}${_id}`)
+    return res
+  },
+  async pagedetails ({ commit }, { _id }) {
+    const res = await axios.get(`${api.pagedetails}${_id}`)
+    if (res.errcode === 0) commit('details', res)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  page (state, payload) {
+    state.pageList = payload.data
+    state.total = payload.total
+  },
+  details (state, payload) {
+    state.pageItem = payload.data
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 41 - 0
src/store/wokes/webconfig.js

@@ -0,0 +1,41 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  configurationquery: '/api/configuration/query',
+  configurationcreate: '/api/configuration/create',
+  configurationupdate: '/api/configuration/update'
+}
+
+// initial state
+const state = () => ({
+  configurationdata: {}
+})
+
+// actions
+const actions = {
+  async configurationquery ({ commit }, { filter, paging } = {}) {
+    const res = await axios.get(api.configurationquery)
+    if (res.errcode === 0) commit('configurationquery', res)
+    return res
+  },
+  async configurationcreate ({ commit }, payload) {
+    const res = await axios.post(api.configurationcreate, payload)
+    return res
+  },
+  async configurationupdate ({ commit }, payload) {
+    const res = await axios.post(api.configurationupdate, payload)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  configurationquery (state, payload) {
+    state.configurationdata = payload.data
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 48 - 0
src/views/frame/Home.vue

@@ -0,0 +1,48 @@
+<template>
+  <div class="wrapper">
+    <div class="titleWrapper">
+      <h2 class="title">{{ productName }}</h2>
+      <p>{{ description }}</p>
+    </div>
+  </div>
+</template>
+
+<script>
+const productName = process.env.VUE_APP_HOME_TITLEE
+const description = process.env.VUE_APP_HOME_DESCRIPTION
+export default {
+  name: 'Home',
+  data () {
+    return {
+      productName,
+      description
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.wrapper {
+  height: 100%;
+  width: 100%;
+  position: relative;
+  background-position: center;
+  background-repeat: o-repeat;
+  background-size: cover;
+  background-image: url('~@/assets/home.png');
+}
+.titleWrapper {
+  text-align: center;
+  top: 40%;
+  width: 100%;
+  position: absolute;
+  color: #1d87b4;
+}
+.title {
+  font-size: 32px;
+  // color: #333;
+  letter-spacing: 1.94px;
+  line-height: 2em;
+  text-align: center;
+}
+</style>

+ 130 - 0
src/views/frame/Login.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="container">
+    <div class="min">
+      <div class="form">
+        <h1>{{ title }}</h1>
+        <el-form :model="loginForm" :rules="rules" ref="loginForm">
+          <el-form-item prop="acct">
+            <el-input v-model="loginForm.acct" placeholder="用户名" prefix-icon="naf-icons naf-icon-user" @keyup.13.native="submitForm('loginForm')">
+            </el-input>
+          </el-form-item>
+          <el-form-item prop="password">
+            <el-input
+              type="password"
+              placeholder="密码"
+              v-model="loginForm.password"
+              prefix-icon="naf-icons naf-icon-password"
+              @keyup.13.native="submitForm('loginForm')"
+            >
+            </el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="submitForm('loginForm')" :style="{ width: '100%' }">登录</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { createNamespacedHelpers } from 'vuex'
+const { mapActions } = createNamespacedHelpers('login')
+const title = process.env.VUE_APP_MENU_TITLE
+export default {
+  data () {
+    return {
+      title,
+      loginForm: {
+        acct: '',
+        password: ''
+      },
+      rules: {
+        acct: [{ required: true, message: '请输入帐号' }],
+        password: [{ required: true, message: '请输入密码' }]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['login']),
+    async submitForm (formName) {
+      this.$refs[formName].validate(async valid => {
+        if (valid) {
+          const res = await this.login({
+            acct: this.loginForm.acct,
+            password: this.loginForm.password
+          })
+          if (res && res.errcode === 0) {
+            sessionStorage.setItem('token', res.token)
+            sessionStorage.setItem('userName', res.userInfo.userName)
+            sessionStorage.setItem('acct', res.userInfo.acct)
+            this.$router.push('/frame')
+          }
+        } else {
+          if (this.loginForm.username === '' || this.loginForm.username == null) {
+            this.$notify.error({
+              title: '错误',
+              message: '请输入用户名',
+              offset: 100
+            })
+            return false
+          }
+          if (this.loginForm.password === '' || this.loginForm.password == null) {
+            this.$notify.error({
+              title: '错误',
+              message: '请输入密码',
+              offset: 100
+            })
+            return false
+          }
+        }
+        return true
+      })
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped>
+h1 {
+  color: #fff;
+  font-weight: 700;
+  text-align: center;
+  letter-spacing: 5px
+}
+.container {
+  background: #f0f2f5;
+  background-image: url('~@/assets/bg2.jpg');
+  height: 100%;
+  width: 100%;
+  min-height: 800px;
+  min-width: 1200px;
+  background-repeat: no-repeat;
+  background-position: center;
+  background-size: 100%;
+  padding: 110px 0 144px 0;
+  position: relative;
+}
+.form {
+  width: 460px;
+  margin: 5% auto;
+}
+.min {
+  // width: 660px;
+  margin: 10% auto;
+  // display: flex;
+}
+.checked {
+  color: #fff;
+  width: 100%;
+  text-align: right;
+}
+.title {
+  font-size: 33px;
+  font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
+  font-weight: 600;
+  position: relative;
+  top: 2px;
+}
+</style>

+ 64 - 0
src/views/frame/demo.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="container">
+    <naf-grid class="grid" @edit="edit" :data="datas" :meta="meta" :total="total"></naf-grid>
+    <dialog-drawer type="drawer" :visible="visible" title="这是标题" @close="visible = false">
+      <template v-slot:content>
+        <naf-form @save="save" :meta="meta" :rules="rules">
+          <template v-slot:field="{ form, item }">
+            <el-input v-model="form[item.name]" />
+          </template>
+        </naf-form>
+      </template>
+    </dialog-drawer>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import dialogDrawer from '@naf/data/dialog -drawer'
+import nafForm from '@naf/data/form'
+export default {
+  components: {
+    nafGrid,
+    dialogDrawer,
+    nafForm
+  },
+  data () {
+    return {
+      visible: false,
+      total: 100,
+      rules: {
+        name: [
+          { required: true, message: '请输入活动名称', trigger: 'blur' },
+          { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
+        ]
+      },
+      meta: [
+        { name: 'name', title: '名字', filter: true },
+        { name: 'age', title: '年龄', filter: true, width: '100' },
+        { name: 'sdate', title: '状态', filter: true, formatter: 'sdate' },
+        { name: 'xb', title: '性别', formatter: 'xb', filter: true, width: '100' },
+        { name: 'sfzh', title: '身份证号' },
+        { name: 'bzkh', title: '保障卡号', filter: true },
+        { name: 'jgzh', title: '军官证件号', slots: true },
+        { name: 'yx', title: '邮箱' }
+      ],
+      datas: [
+        { name: '张三', age: '24', sdate: '1', xb: '0', sfzh: '220222356877563256', bzkh: '', jgzh: '', yx: '13526545484984@qq.com' },
+        { name: '张三2', age: '23', sdate: '0', xb: '0', sfzh: '220222356877563256', bzkh: '', jgzh: '', yx: '13526545484984@qq.com' }
+      ]
+    }
+  },
+  methods: {
+    edit (e) {
+      this.visible = true
+    },
+    save (e) {
+      console.log(e)
+    }
+  },
+  mounted () {}
+}
+</script>
+
+<style lang="less" scoped></style>

+ 121 - 0
src/views/frame/frame.vue

@@ -0,0 +1,121 @@
+<template>
+  <el-container class="layout"  :style="{ minHeight: confing.height, minWidth: confing.width }">
+    <!-- 页头 -->
+    <el-header :style="{ backgroundColor: confing.backgroundColor }">
+      <!-- logo -->
+      <div class="logo" :style="{ width: !isCollapse ? confing.menuWidth : '64px' }">
+        <img src="../../assets/logo1.svg" class="logoImg">
+        <h3 v-if="!isCollapse">{{ confing.title }}</h3>
+      </div>
+      <!-- 菜单样式切换按钮 -->
+      <el-button @click="isCollapse = !isCollapse" class="btn" size="medium" type="primary" :icon="!isCollapse ? 'el-icon-s-fold' : 'el-icon-s-unfold'"></el-button>
+      <!-- 用户头像 -->
+      <naf-user class="userBox"></naf-user>
+    </el-header>
+    <el-container>
+      <!-- 菜单 -->
+      <el-aside :width="!isCollapse ? confing.menuWidth : ''">
+        <naf-menu :menuItems="menuItems" :isCollapse="isCollapse"></naf-menu>
+      </el-aside>
+      <el-main>
+        <!-- 面包屑 -->
+        <naf-breadcrumb :menuItems="userMenuList"></naf-breadcrumb>
+        <div class="content" id="content">
+          <!-- 页面主体 -->
+          <router-frame :menuItems="userMenuList"></router-frame>
+        </div>
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script>
+import nafMenu from '@naf/layout/menu'
+import nafUser from '@naf/layout/user'
+import nafBreadcrumb from '@naf/layout/breadcrumb'
+import routerFrame from './router-frame'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('menu')
+const confing = {
+  width: process.env.VUE_APP_WIDTH,
+  height: process.env.VUE_APP_HEIGHT,
+  menuWidth: process.env.VUE_APP_MENU_WIDTH,
+  backgroundColor: process.env.VUE_APP_MENU_BACKGROUNDCOLOR,
+  title: process.env.VUE_APP_MENU_TITLE,
+  tabs: process.env.VUE_APP_TABS || 'false'
+}
+export default {
+  components: {
+    nafMenu,
+    nafUser,
+    nafBreadcrumb,
+    routerFrame
+  },
+  data () {
+    return {
+      confing,
+      isCollapse: false
+    }
+  },
+  methods: {
+    ...mapActions(['getusermenu'])
+  },
+  mounted () {
+    this.getusermenu()
+  },
+  computed: {
+    ...mapState(['userMenuList']),
+    // 菜单计算属性
+    menuItems () {
+      if (this.userMenuList.length <= 0) return []
+      // 返回树形结构
+      return this.$deepTree(this.userMenuList)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.layout {
+  overflow: hidden;
+  .el-header {
+    padding: 0;
+    display: flex;
+    position: relative;
+    .logo {
+      border-right: 1px solid #fff;
+      height: 100%;
+      color: #fff;
+      display: flex;
+      h3 {
+        margin-left: 5%;
+      }
+      .logoImg {
+        height: 70%;
+        margin-top: 3%;
+        margin-left: 3%;
+      }
+    }
+    .btn {
+      height: 60%;
+      margin-top: 1em;
+      margin-left: 1em;
+    }
+    .userBox {
+      position: absolute;
+      width: 7%;
+      right: 1%;
+    }
+  }
+  /deep/ .el-main {
+    padding: 0;
+  }
+  .content {
+    display: flex;
+    height: 96.5%;
+    .grid {
+      width: 85%;
+    }
+  }
+}
+</style>

+ 142 - 0
src/views/frame/router-frame.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="container">
+    <el-tabs v-if="showTabs == 'true'" v-model="editableTabsValue" type="card" closable @tab-remove="removeTab" @tab-click="tabClick">
+      <el-tab-pane label="首页" name="home">
+        <home></home>
+      </el-tab-pane>
+      <el-tab-pane
+        v-for="item in editableTabs"
+        :key="item.id"
+        :label="item.title"
+        :name="item.id"
+      >
+        <router-view />
+      </el-tab-pane>
+    </el-tabs>
+    <router-view v-if="showTabs !== 'true'" />
+  </div>
+</template>
+
+<script>
+import home from './Home'
+const showTabs = process.env.VUE_APP_TABS || 'false'
+export default {
+  props: {
+    // 所有菜单
+    menuItems: Array
+  },
+  components: {
+    home
+  },
+  computed: {
+    routePath () {
+      return this.$route.path
+    }
+  },
+  data () {
+    return {
+      // 是否显示标签页
+      showTabs,
+      // 默认标签页
+      editableTabsValue: 'home',
+      // 所有标签页
+      editableTabs: []
+    }
+  },
+  methods: {
+    // tabs 移除标签操作
+    removeTab (targetName) {
+      if (targetName === 'home') {
+        this.$message.error('默认标签不能移除')
+        return false
+      }
+      const tabs = this.editableTabs
+      let activeName = this.editableTabsValue
+      // 如果移除的标签 = 当前显示的标签
+      if (activeName === targetName) {
+        // 轮询所有标签
+        tabs.forEach((tab, index) => {
+          // 如果标签名相等
+          if (tab.id === targetName) {
+            // 设置下一个标签
+            const nextTab = tabs[index + 1] || tabs[index - 1]
+            // 如果下一个标签存在
+            if (nextTab) {
+              const url = `/frame${nextTab.path}`
+              // 如果当前路由与下一个标签路由相等就返回
+              if (url === this.$route.path) return
+              // 否则就跳转下一个路由
+              this.$router.push(url)
+              // 标签页显示下一个
+              activeName = nextTab.id
+            } else {
+              // 如果没有下一个就跳转到默认页
+              this.$router.push('/frame')
+              activeName = 'home'
+            }
+          }
+        })
+      }
+      this.editableTabsValue = activeName
+      // 过滤出剩下的标签页
+      this.editableTabs = tabs.filter(tab => tab.id !== targetName)
+    },
+    // 点击标签页
+    tabClick ({ name }) {
+      // 过滤出当前标签页
+      const item = this.editableTabs.filter(tab => tab.id === name)
+      const url = name === 'home' ? '/frame' : `/frame${item[0].path}`
+      // 如果当前路由与过滤出的路由相等就返回
+      if (url === this.$route.path) return
+      this.$router.push(url)
+    }
+  },
+  mounted () {
+    if (this.$route.path === '/frame') return false
+  },
+  watch: {
+    // 监听计算属性(当前路由)
+    routePath (val) {
+      // 过滤出当前路由对应的标签页
+      const items = this.editableTabs.filter(p => {
+        const itemPath = `${this.$route.path}`
+        return `/frame${p.path}` === itemPath
+      })
+      // 如果标签页存在
+      if (items.length > 0 || this.$route.path === '/frame') {
+        // 如果是当前路由是首页  跳转首页
+        if (this.$route.path === '/frame') {
+          this.editableTabsValue = 'home'
+        } else {
+          // 否则跳转到路由匹配出的标签页
+          this.editableTabsValue = items[0].id
+        }
+        return false
+      }
+      // 如果标签页不存在
+      const path = this.$route.path
+      // 完整菜单过滤出当前路由的菜单
+      const item = this.menuItems.filter(p => `/frame${p.path}` === path)
+      // 放到标签页
+      this.editableTabs.push(...item)
+      // 显示过滤出的标签页
+      this.editableTabsValue = item[0].id
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  width: 100%;
+  /deep/ .el-tabs {
+    height: 100%;
+  }
+  /deep/ .el-tabs__content {
+    height: 93%;
+    /deep/ .el-tab-pane {
+      height: 100%;
+    }
+  }
+}
+</style>

+ 56 - 0
src/views/gaf/log.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>日志管理</span>
+      </div>
+      <div class="main">
+        <naf-grid class="grid" ref="grid" :data="logList" :meta="meta" :total="total" @query="query" :readonly="false"></naf-grid>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('log')
+export default {
+  components: {
+    nafGrid
+  },
+  data () {
+    return {
+      meta: [
+        { name: 'mondel', title: '模块', filter: true },
+        { name: 'method', title: '操作', filter: true },
+        { name: 'result', title: '状态', filter: true, formatter: 'result' },
+        { name: 'date', title: '时间', filter: true },
+        { name: 'userName', title: '操作人', filter: true }
+      ]
+    }
+  },
+  methods: {
+    ...mapActions(['logquery']),
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.logquery({ filter, paging })
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'logList'])
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+}
+</style>

+ 142 - 0
src/views/gaf/role.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>系统角色管理</span>
+        <el-button style="float: right; padding: 3px 0" type="text" @click="adduser">添加角色</el-button>
+      </div>
+      <div class="main">
+        <naf-grid class="grid" ref="grid" @edit="edit" @delete="deleterole" :data="roleList" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+    <dialog-drawer type="dialog" :visible="visible" :title="isNew ? '修改角色' : '添加角色'" @close="close" :width="'30%'">
+      <template v-slot:content>
+        <naf-form v-if="visible" ref="ruleForm" @save="save" :meta="formmeta" :rules="rules" :data="is_data"></naf-form>
+      </template>
+    </dialog-drawer>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import dialogDrawer from '@naf/data/dialog -drawer'
+import nafForm from '@naf/data/form'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('role')
+export default {
+  components: {
+    nafGrid,
+    dialogDrawer,
+    nafForm
+  },
+  data () {
+    return {
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'name', title: '角色名称', filter: true },
+        { name: 'code', title: '角色编码', filter: true },
+        { name: 'state', title: '状态', formatter: 'state' }
+      ],
+      formmeta: [
+        { name: 'name', title: '角色名称' },
+        { name: 'code', title: '角色编码', disabled: false },
+        { name: 'state', title: '状态', formatter: 'state' }
+      ],
+      rules: {
+        name: [
+          { required: true, message: '请输入角色名称', trigger: 'blur' }
+        ],
+        code: [
+          { required: true, message: '请输入角色编码', trigger: 'blur' }
+        ],
+        state: [
+          { required: true, message: '请输入状态', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['rolequery', 'rolecreate', 'roleupdate', 'roledelete']),
+    // 添加
+    adduser () {
+      this.is_data = {}
+      this.visible = true
+      // eslint-disable-next-line no-return-assign
+      this.formmeta.map(p => p.name === 'code' ? p.disabled = false : true)
+    },
+    // 删除
+    async deleterole (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.roledelete(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          this.query()
+          this.$refs.grid.resetpage(-1)
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 修改
+    edit (e) {
+      this.is_data = e
+      this.visible = true
+      // eslint-disable-next-line no-return-assign
+      this.formmeta.map(p => p.name === 'code' ? p.disabled = true : false)
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.rolequery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      if (this.isNew) {
+        // 修改
+        res = await this.roleupdate(e)
+      } else {
+        // 添加
+        res = await this.rolecreate(e)
+      }
+      // eslint-disable-next-line eqeqeq
+      if (res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.query()
+        this.visible = false
+        this.$refs.grid.resetpage(-1)
+      }
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'roleList']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+}
+</style>

+ 225 - 0
src/views/gaf/rolepower.vue

@@ -0,0 +1,225 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>角色授权管理</span>
+      </div>
+      <div class="main">
+        <naf-grid class="grid" :operation="operation" @menu="bindmenu" :data="roleList" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+    <dialog-drawer type="dialog" :visible="visible" @close="close">
+      <template v-slot:content>
+        <el-tabs type="border-card" v-model="activeName" @tab-remove="taRemove">
+          <el-tab-pane label="菜单列表" name="menulist" :disabled="activeName == 'menuselection'">
+            <el-card class="box-card">
+              <div slot="header" class="clearfix">
+                <el-button style="float: right; padding: 0" type="text" @click="menubind">绑定菜单</el-button>
+              </div>
+              <div class="main">
+                <naf-grid class="grid" :selection="false" :search="false" :pagination="false" :operation="operation2" @delete="unbind" :data="menus" :meta="rolemeta"></naf-grid>
+              </div>
+            </el-card>
+          </el-tab-pane>
+          <el-tab-pane name="menuselection" label="绑定菜单" v-if="tab_pane" :closable="true">
+            <el-card class="box-card">
+              <div slot="header" class="clearfix">
+                <el-button style="float: right; padding: 0" type="text" @click="rolebindmenu">保存</el-button>
+              </div>
+              <div class="main">
+                <el-tree
+                  :data="menutree"
+                  show-checkbox
+                  node-key="id"
+                  :props="defaultProps"
+                  ref="tree"
+                  :check-strictly="true"
+                  :default-checked-keys="keylist"
+                >
+                </el-tree>
+              </div>
+            </el-card>
+          </el-tab-pane>
+        </el-tabs>
+      </template>
+    </dialog-drawer>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import dialogDrawer from '@naf/data/dialog -drawer'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('role')
+const { mapState: menumapState, mapActions: menumapActions } = createNamespacedHelpers('menu')
+export default {
+  components: {
+    nafGrid,
+    dialogDrawer
+  },
+  data () {
+    return {
+      // 默认选中
+      keylist: [],
+      // 菜单多选
+      selection_menu_list: [],
+      activeName: 'menulist',
+      tab_pane: false,
+      roleInfo: null,
+      visible: false,
+      defaultProps: {
+        children: 'children',
+        label: 'title'
+      },
+      // 角色列表
+      meta: [
+        { name: 'name', title: '角色名称', filter: true },
+        { name: 'code', title: '角色编码', filter: true },
+        { name: 'state', title: '状态', formatter: 'state' }
+      ],
+      // 菜单列表
+      rolemeta: [
+        { name: 'title', title: '菜单名称' },
+        { name: 'id', title: '菜单Id' }
+      ],
+      // 角色列表按钮
+      operation: [
+        { name: 'menu', title: '绑定菜单', icons: 'el-icon-tickets' }
+      ],
+      // 菜单列表按钮
+      operation2: [
+        { name: 'delete', title: '删除', icons: 'el-icon-delete' }
+      ]
+    }
+  },
+  methods: {
+    ...mapActions(['rolequery', 'roleupdate']),
+    ...menumapActions(['getmenu']),
+    // 标签移除
+    taRemove (e) {
+      this.tab_pane = false
+      this.activeName = 'menulist'
+    },
+    // 绑定菜单按钮
+    async bindmenu (e) {
+      console.log(e)
+      this.roleInfo = e
+      this.visible = true
+      this.getbindmenu()
+    },
+    // 查询角色
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.rolequery({ filter, paging })
+    },
+    // 查询菜单
+    async getbindmenu () {
+      await this.getmenu()
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+      this.activeName = 'menulist'
+      this.tab_pane = false
+    },
+    // 点击添加菜单按钮
+    menubind (e) {
+      this.keylist = this.roleInfo.adminMenuList
+      this.activeName = 'menuselection'
+      this.tab_pane = true
+    },
+    // 解除绑定
+    async unbind (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const parent = this.menus.filter(p => p.parentId === e.id)
+        if (parent.length > 0) {
+          this.$message.error('请先删除下级')
+          return false
+        }
+        // 解除绑定菜单
+        const adminMenuList = this.roleInfo.adminMenuList.filter(p => p !== e.id)
+        this.roleInfo.adminMenuList = adminMenuList
+        const res = await this.roleupdate(this.roleInfo)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          this.activeName = 'menulist'
+          this.tab_pane = false
+          this.getbindmenu()
+          this.query()
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 保存
+    async rolebindmenu () {
+      const list = this.$refs.tree.getCheckedKeys()
+      const menulist = [...list]
+      list.filter(p => {
+        if (p[0].indexOf('1') !== -1) {
+          if (!menulist.includes('1')) {
+            menulist.push('1')
+          }
+        }
+        if (p[0].indexOf('2') !== -1) {
+          if (!menulist.includes('2')) {
+            menulist.push('2')
+          }
+        }
+        if (p[0].indexOf('3') !== -1) {
+          if (!menulist.includes('3')) {
+            menulist.push('3')
+          }
+        }
+      })
+      this.roleInfo.adminMenuList = menulist
+      const res = await this.roleupdate(this.roleInfo)
+      // eslint-disable-next-line eqeqeq
+      if (res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.activeName = 'menulist'
+        this.tab_pane = false
+        this.query()
+      }
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'roleList']),
+    ...menumapState(['menuList']),
+    // 过滤已绑定
+    menus () {
+      const list = []
+      if (this.roleInfo !== null) {
+        this.menuList.filter(p => {
+          if (this.roleInfo.adminMenuList.includes(p.id)) list.push(p)
+        })
+      }
+      return list
+    },
+    // 计算树形菜单
+    menutree () {
+      return this.$deepTree(this.menuList)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+}
+
+</style>

+ 164 - 0
src/views/gaf/user.vue

@@ -0,0 +1,164 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>系统用户管理</span>
+        <el-button style="float: right; padding: 3px 0" type="text" @click="adduser">添加用户</el-button>
+      </div>
+      <div class="main">
+        <naf-grid ref="grid" class="grid" @edit="edit" @delete="deleteusr" :data="userList" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+    <dialog-drawer type="dialog" :visible="visible" :title="isNew ? '修改用户' : '添加用户'" @close="close" :width="'30%'">
+      <template v-slot:content>
+        <naf-form v-if="visible" ref="ruleForm" @save="save" :meta="formmeta" :rules="rules" :data="is_data"></naf-form>
+      </template>
+    </dialog-drawer>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import dialogDrawer from '@naf/data/dialog -drawer'
+import nafForm from '@naf/data/form'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('adminuser')
+export default {
+  components: {
+    nafGrid,
+    dialogDrawer,
+    nafForm
+  },
+  data () {
+    return {
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'userName', title: '名字', filter: true },
+        { name: 'acct', title: '帐号', filter: true },
+        { name: 'state', title: '状态', formatter: 'state' },
+        { name: 'phone', title: '手机号' }
+      ],
+      formmeta: [
+        { name: 'userName', title: '姓名' },
+        { name: 'acct', title: '帐号', disabled: false },
+        { name: 'password', title: '密码' },
+        { name: 'passwordtowo', title: '确认密码' },
+        { name: 'state', title: '状态', formatter: 'state' },
+        { name: 'phone', title: '手机号', type: 'number' }
+      ],
+      rules: {
+        userName: [
+          { required: true, message: '请输入姓名', trigger: 'blur' }
+        ],
+        acct: [
+          { required: true, message: '请输入帐号', trigger: 'blur' }
+        ],
+        state: [
+          { required: true, message: '请输入状态', trigger: 'blur' }
+        ],
+        password: [
+          { required: true, message: '请输入密码', trigger: 'blur' }
+        ],
+        passwordtowo: [
+          { required: true, message: '请输入确认密码', trigger: 'blur' }
+        ],
+        phone: [
+          { type: 'number', message: '请输入数字' }
+          // { min: 10, max: 11, message: '长度在为11个字符', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['getUser', 'usercreate', 'userupdate', 'userdelete']),
+    // 添加
+    adduser () {
+      this.is_data = {}
+      this.visible = true
+      // eslint-disable-next-line no-return-assign
+      this.formmeta.map(p => p.name === 'acct' ? p.disabled = false : true)
+    },
+    // 删除
+    async deleteusr (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.userdelete(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.error(res.errmsg)
+        } else {
+          this.$message.success('操作成功')
+          this.query()
+          this.$refs.grid.resetpage(-1)
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 修改
+    edit (e) {
+      this.is_data = e
+      this.visible = true
+      this.rules.password = false
+      this.rules.passwordtowo = false
+      // eslint-disable-next-line no-return-assign
+      this.formmeta.map(p => p.name === 'acct' ? p.disabled = true : false)
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.getUser({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      if (e.passwordtowo !== e.password) {
+        this.$message.error('两次密码输入不一致')
+        return
+      }
+      let res
+      if (this.isNew) {
+        // 修改用户
+        res = await this.userupdate(e)
+      } else {
+        // 添加用户
+        res = await this.usercreate(e)
+      }
+      // eslint-disable-next-line eqeqeq
+      if (res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.query()
+        this.visible = false
+        this.$refs.grid.resetpage(-1)
+      }
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'userList']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+}
+</style>

+ 221 - 0
src/views/gaf/userpower.vue

@@ -0,0 +1,221 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>用户授权管理</span>
+      </div>
+      <div class="main">
+        <naf-grid class="grid" :operation="operation" @role="bindrole" :data="userList" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+    <dialog-drawer type="dialog" :visible="visible" @close="close">
+      <template v-slot:content>
+        <el-tabs type="border-card" v-model="activeName" @tab-remove="taRemove" @tab-click="tabClick">
+          <el-tab-pane label="角色列表" name="rolelist" :disabled="activeName == 'roleselection' || activeName == 'columnselection'">
+            <el-card class="box-card">
+              <div slot="header" class="clearfix">
+                <el-button style="float: right; padding: 0" type="text" @click="userbind(true)">添加角色</el-button>
+              </div>
+              <div class="main">
+                <naf-grid class="grid" :selection="false" :search="false" :operation="operation2" @delete="unbind" :data="roles.list" :meta="rolemeta" :total="roles.total" @query="getbindrole"></naf-grid>
+              </div>
+            </el-card>
+          </el-tab-pane>
+          <el-tab-pane name="roleselection" label="角色选择" v-if="is_role && tab_pane" :closable="true">
+            <el-card class="box-card">
+              <div slot="header" class="clearfix">
+                <el-button style="float: right; padding: 0" type="text" @click="userbindrole">保存</el-button>
+              </div>
+              <div class="main">
+                <naf-grid class="grid" @selection="selectionrole" :search="false" :readonly="false" :data="notrole.list" :meta="rolemeta" :total="notrole.total" @query="getbindrole"></naf-grid>
+              </div>
+            </el-card>
+          </el-tab-pane>
+        </el-tabs>
+      </template>
+    </dialog-drawer>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import dialogDrawer from '@naf/data/dialog -drawer'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('adminuser')
+const { mapState: rolemapState, mapActions: rolemapActions } = createNamespacedHelpers('role')
+export default {
+  components: {
+    nafGrid,
+    dialogDrawer
+  },
+  data () {
+    return {
+      // 栏目多选
+      selection_column_list: [],
+      // 角色多选
+      selection_role_list: [],
+      activeName: 'rolelist',
+      tab_pane: false,
+      userInfo: null,
+      is_role: false,
+      visible: false,
+      // 用户列表
+      meta: [
+        { name: 'userName', title: '名字', filter: true },
+        { name: 'acct', title: '帐号', filter: true },
+        { name: 'state', title: '状态', formatter: 'state' },
+        { name: 'phone', title: '手机号' }
+      ],
+      // 角色列表
+      rolemeta: [
+        { name: 'name', title: '角色名字' },
+        { name: 'code', title: '角色编码' }
+      ],
+      // 角色列表
+      columnmeta: [
+        { name: 'name', title: '栏目名称' },
+        { name: 'code', title: '栏目编码' }
+      ],
+      // 用户列表按钮
+      operation: [
+        { name: 'role', title: '角色授权', icons: 'el-icon-user' }
+        // { name: 'column', title: '栏目授权', icons: 'el-icon-tickets' }
+      ],
+      // 角色列表按钮
+      operation2: [
+        { name: 'delete', title: '删除', icons: 'el-icon-delete' }
+      ]
+    }
+  },
+  methods: {
+    ...mapActions(['getUser', 'userupdate']),
+    ...rolemapActions(['rolequery']),
+    // 点击标签
+    tabClick (e) {
+      if (e === 'rolelist') {
+        this.getbindrole()
+      }
+    },
+    // 标签移除
+    taRemove (e) {
+      this.tab_pane = false
+      if (e === 'roleselection') {
+        this.activeName = 'rolelist'
+      }
+    },
+    // 绑定角色按钮
+    async bindrole (e) {
+      this.userInfo = e
+      this.visible = true
+      this.is_role = true
+      this.activeName = 'rolelist'
+      this.getbindrole()
+    },
+    // 查询用户
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.getUser({ filter, paging })
+    },
+    // 查询角色
+    async getbindrole ({ filter = {}, paging = {} } = {}) {
+      await this.rolequery({ filter, paging })
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+    },
+    // 点击添加按钮
+    userbind (e) {
+      this.is_role = e
+      if (this.is_role) {
+        // 角色
+        this.activeName = 'roleselection'
+        this.getbindrole()
+      }
+      this.tab_pane = true
+    },
+    // 解除绑定
+    async unbind (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        if (this.is_role) {
+        // 解除绑定角色
+          const code = e.code
+          const roleList = this.userInfo.roleList.filter(p => p !== code)
+          this.userInfo.roleList = roleList
+          const res = await this.userupdate(this.userInfo)
+          // eslint-disable-next-line eqeqeq
+          if (res.errcode == 0) {
+            this.$message.success('操作成功')
+            this.activeName = 'rolelist'
+            this.tab_pane = false
+          }
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 角色多选返回
+    selectionrole (e) {
+      this.selection_role_list = e
+    },
+    // 绑定角色
+    async userbindrole () {
+      const list = []
+      this.selection_role_list.filter(p => {
+        list.push(p.code)
+      })
+      this.userInfo.roleList.push(...list)
+      const res = await this.userupdate(this.userInfo)
+      // eslint-disable-next-line eqeqeq
+      if (res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.activeName = 'rolelist'
+        this.tab_pane = false
+      }
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'userList']),
+    ...rolemapState({ roletotal: 'total', roleList: 'roleList' }),
+    // 角色过滤已绑定
+    roles () {
+      const list = []
+      if (this.userInfo !== null) {
+        this.roleList.filter(p => {
+          if (this.userInfo.roleList.includes(p.code)) list.push(p)
+        })
+      }
+      return { list, total: list.length || 0 }
+    },
+    // 角色过滤未绑定
+    notrole () {
+      const list = []
+      if (this.userInfo !== null) {
+        this.roleList.filter(p => {
+          if (!this.userInfo.roleList.includes(p.code)) list.push(p)
+        })
+      }
+      return { list, total: list.length || 0 }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+}
+
+</style>

+ 298 - 0
src/views/wokes/configuration/banner.vue

@@ -0,0 +1,298 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>轮播图管理</span>
+        <el-button style="float: right; padding: 3px 0" type="text" @click="addbanner">添加轮播图</el-button>
+      </div>
+      <div class="main">
+        <naf-grid ref="grid" class="grid" @edit="edit" @delete="deletebanner" :data="bannerList" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+    <el-card class="box-dj" v-if="visible">
+      <div slot="header" class="clearfix">
+        <span>{{ isNew ? '修改轮播图' : '添加轮播图' }}</span>
+      </div>
+      <div class="main">
+        <naf-form v-if="visible" ref="ruleForm" @save="save" :meta="formmeta" :rules="rules" :data="is_data" :close="true" @close="close">
+          <template v-slot:field="{ form, item }">
+            <!-- 图片上传 -->
+            <el-upload
+              v-if="item.name == 'path'"
+              class="avatar-uploader avatar"
+              action="/api/files/upload"
+              :show-file-list="false"
+              :on-success="handleAvatarSuccess"
+              :before-upload="beforeAvatarUpload"
+              :data="{ type: 'resource' }"
+              :headers="myHeaders"
+            >
+              <img v-if="imageUrl" :src="imageUrl" class="avatar">
+              <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+            </el-upload>
+            <!-- 选择日期 -->
+            <el-date-picker
+              value-format="yyyy-MM-dd"
+              v-model="form[item.name]"
+              v-if="item.name == 'date'"
+              type="date"
+              placeholder="选择日期">
+            </el-date-picker>
+            <!-- 附件上传 -->
+            <el-upload
+              v-if="item.name == 'annex'"
+              class="upload-demo"
+              action="/api/files/upload"
+              :limit="1"
+              :on-exceed="handleExceed"
+              :on-success="handleFilesSuccess"
+              :before-upload="beforeFilesUpload"
+              :before-remove="beforefileremove"
+              :data="{ type: 'files' }"
+              :headers="myHeaders"
+              :file-list="fileList">
+              <el-button size="small" type="primary">点击上传</el-button>
+            </el-upload>
+            <!-- 富文本 -->
+            <editor-bar v-if="item.name == 'content'" v-model="form[item.name]" :isClear="isClear"></editor-bar>
+          </template>
+        </naf-form>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import nafForm from '@naf/data/form'
+import editorBar from '@naf/data/editoritem'
+import { createNamespacedHelpers } from 'vuex'
+const token = sessionStorage.getItem('token')
+const { mapState, mapActions } = createNamespacedHelpers('banner')
+export default {
+  components: {
+    nafGrid,
+    nafForm,
+    editorBar
+  },
+  data () {
+    return {
+      isClear: false,
+      detail: '',
+      myHeaders: { Authorization: `Bearer ${token}` },
+      imageUrl: '',
+      fileList: [],
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'title', title: '标题', filter: true },
+        { name: 'date', title: '发表日期' }
+      ],
+      formmeta: [
+        { name: 'path', title: '轮播图', slots: 'field' },
+        { name: 'title', title: '标题' },
+        { name: 'date', title: '发表日期', slots: 'field' },
+        { name: 'annex', title: '附件', slots: 'field' },
+        { name: 'content', title: '内容', slots: 'field' }
+      ],
+      rules: {
+        title: [
+          { required: true, message: '请输入标题', trigger: 'blur' }
+        ],
+        path: [
+          { required: true, message: '请上传轮播图', trigger: 'blur' }
+        ],
+        content: [
+          { required: true, message: '请输入内容', trigger: 'blur' }
+        ],
+        date: [
+          { required: true, message: '请选择日期', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['bannerquery', 'bannercreate', 'bannerupdate', 'bannerdelete', 'bannerdetails']),
+    // 添加
+    async addbanner () {
+      this.is_data = {}
+      this.visible = true
+    },
+    // 删除
+    async deletebanner (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.bannerdelete(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          this.query()
+          this.$refs.grid.resetpage(-1)
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 修改
+    async edit (e) {
+      await this.bannerdetails({ _id: e._id })
+      this.is_data = { ...this.bannerItem }
+      if (this.is_data.annex !== '' && this.is_data.annex !== null) this.fileList.push({ name: this.is_data.annexname, url: this.is_data.annex })
+      if (this.bannerItem.path) this.imageUrl = this.bannerItem.path
+      this.visible = true
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.bannerquery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      if (this.isNew) {
+        // 修改
+        res = await this.bannerupdate(e)
+      } else {
+        // 添加
+        res = await this.bannercreate(e)
+      }
+      // eslint-disable-next-line eqeqeq
+      if (res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.query()
+        this.$refs.grid.resetpage(-1)
+        this.visible = false
+        this.fileList = []
+        this.imageUrl = ''
+      }
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+      this.fileList = []
+      this.imageUrl = ''
+    },
+    // 文件上传
+    // 文件列表移除文件时的钩子
+    beforefileremove (file, fileList) {
+      this.is_data.annex = null
+      this.$refs.ruleForm.form.annex = null
+      this.is_data.annexname = null
+      this.$refs.ruleForm.form.annexname = null
+    },
+    // 文件上传成功时的钩子
+    handleFilesSuccess (res, file) {
+      this.is_data = this.$refs.ruleForm.form
+      this.is_data.annex = res.data.path
+      this.is_data.annexname = res.data.name
+    },
+    // 文件上传之前的钩子
+    beforeFilesUpload (file) {
+      const isType = file.type === 'image/jpeg' || 'image/png' || 'application/octet-stream' || 'application/zip' || 'application/octet-stream' || 'application/msword' || 'application/vnd.ms-excel' || 'application/x-zip-compressed' || 'application/pdf'
+      const isLt2M = file.size / 1024 / 1024 < 5
+      if (!isType) {
+        this.$message.error('请上传正确格式')
+      }
+      if (!isLt2M) {
+        this.$message.error('上传图片大小不能超过 5MB!')
+      }
+      return isType && isLt2M
+    },
+    // 文件超出个数限制时的钩子
+    handleExceed (files, fileList) {
+      this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
+    },
+    // 图片上传
+    // 文件上传成功时的钩子
+    handleAvatarSuccess (res, file) {
+      this.is_data = this.$refs.ruleForm.form
+      this.is_data.path = res.data.path
+      this.$refs.ruleForm.formChage('path', res.data.path)
+      this.imageUrl = URL.createObjectURL(file.raw)
+    },
+    // 上传文件之前的钩子
+    beforeAvatarUpload (file) {
+      const isJPG = file.type === 'image/jpeg'
+      const isPNG = file.type === 'image/png'
+      const isLt2M = file.size / 1024 / 1024 < 2
+
+      if (!isJPG && !isPNG) {
+        this.$message.error('上传图片只能是 JPG 或 PNG 格式!')
+      }
+      if (!isLt2M) {
+        this.$message.error('上传图片大小不能超过 2MB!')
+      }
+      return (isJPG || isPNG) && isLt2M
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'bannerList', 'bannerItem']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+  position: relative;
+}
+.box-card {
+  height: 100%;
+}
+.box-dj {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  left: 0;
+  top: 0;
+  z-index: 999;
+  /deep/ .el-card__body {
+    height: 100%;
+    overflow-y: auto;
+    width: 100%;
+    .main {
+      width: 60%;
+      margin: 0 auto;
+      margin-bottom: 4em;
+    }
+  }
+}
+.box-card {
+  height: 100%;
+}
+.avatar-uploader {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+}
+.avatar-uploader .el-upload:hover {
+  border-color: #409EFF;
+}
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  line-height: 178px;
+  text-align: center;
+}
+.avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+
+</style>

+ 205 - 0
src/views/wokes/configuration/links.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>友情链接管理</span>
+        <el-button style="float: right; padding: 3px 0" type="text" @click="adduser">添加友情链接</el-button>
+      </div>
+      <div class="main">
+        <naf-grid ref="grid" @edit="edit" @delete="deletelinks" :data="linksList" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+    <dialog-drawer type="dialog" :visible="visible" :title="isNew ? '修改友情链接' : '添加友情链接'" @close="close" :width="'30%'">
+      <template v-slot:content>
+        <naf-form v-if="visible" ref="ruleForm" @save="save" :meta="formmeta" :rules="rules" :data="is_data">
+          <template v-slot:field="{ item }">
+            <!-- 图片上传 -->
+            <el-upload
+              v-if="item.name == 'path'"
+              class="avatar-uploader avatar"
+              action="/api/files/upload"
+              :show-file-list="false"
+              :on-success="handleAvatarSuccess"
+              :before-upload="beforeAvatarUpload"
+              :data="{ type: 'resource' }"
+              :headers="myHeaders"
+            >
+              <img v-if="imageUrl" :src="imageUrl" class="avatar">
+              <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+            </el-upload>
+          </template>
+        </naf-form>
+      </template>
+    </dialog-drawer>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import dialogDrawer from '@naf/data/dialog -drawer'
+import nafForm from '@naf/data/form'
+import { createNamespacedHelpers } from 'vuex'
+const token = sessionStorage.getItem('token')
+const { mapState, mapActions } = createNamespacedHelpers('links')
+export default {
+  components: {
+    nafGrid,
+    dialogDrawer,
+    nafForm
+  },
+  data () {
+    return {
+      imageUrl: '',
+      is_data: {},
+      visible: false,
+      myHeaders: { Authorization: `Bearer ${token}` },
+      meta: [
+        { name: 'name', title: '链接名称', filter: true },
+        { name: 'path', title: '图片地址' },
+        { name: 'links', title: '访问地址' }
+      ],
+      formmeta: [
+        { name: 'path', title: '链接图片', slots: 'field' },
+        { name: 'name', title: '链接名称' },
+        { name: 'links', title: '访问地址' }
+      ],
+      rules: {
+        name: [
+          { required: true, message: '请输入链接名称', trigger: 'blur' }
+        ],
+        path: [
+          { required: true, message: '请输入链接图片', trigger: 'blur' }
+        ],
+        links: [
+          { required: true, message: '请输入访问地址', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['linksquery', 'linkscreate', 'linksupdate', 'linksdelete']),
+    // 添加
+    adduser () {
+      this.is_data = {}
+      this.visible = true
+    },
+    // 删除
+    async deletelinks (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.linksdelete(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          this.query()
+          this.$refs.grid.resetpage(-1)
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 修改
+    edit (e) {
+      this.is_data = e
+      this.visible = true
+      if (e.path) this.imageUrl = e.path
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.linksquery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      if (this.isNew) {
+        // 修改
+        res = await this.linksupdate(e)
+      } else {
+        // 添加
+        res = await this.linkscreate(e)
+      }
+      // eslint-disable-next-line eqeqeq
+      if (res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.query()
+        this.visible = false
+        this.$refs.grid.resetpage(-1)
+        this.imageUrl = ''
+      }
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+      this.imageUrl = ''
+    },
+    // 图片上传
+    // 文件上传成功时的钩子
+    handleAvatarSuccess (res, file) {
+      this.is_data = this.$refs.ruleForm.form
+      this.is_data.path = res.data.path
+      this.imageUrl = URL.createObjectURL(file.raw)
+    },
+    // 上传文件之前的钩子
+    beforeAvatarUpload (file) {
+      const isJPG = file.type === 'image/jpeg'
+      const isPNG = file.type === 'image/png'
+      const isLt2M = file.size / 1024 / 1024 < 2
+
+      if (!isJPG && !isPNG) {
+        this.$message.error('上传图片只能是 JPG 或 PNG 格式!')
+      }
+      if (!isLt2M) {
+        this.$message.error('上传图片大小不能超过 2MB!')
+      }
+      return (isJPG || isPNG) && isLt2M
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'linksList']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+}
+.avatar-uploader {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+}
+.avatar-uploader .el-upload:hover {
+  border-color: #409EFF;
+}
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  line-height: 178px;
+  text-align: center;
+}
+.avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+</style>

+ 113 - 0
src/views/wokes/configuration/webconfig.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>站点配置</span>
+      </div>
+      <div class="main">
+        <naf-form ref="ruleForm" @save="save" :meta="formmeta" :rules="rules" :data="configurationdata"></naf-form>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import nafForm from '@naf/data/form'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('webconfig')
+export default {
+  components: {
+    nafForm
+  },
+  data () {
+    return {
+      is_data: {},
+      formmeta: [
+        { name: 'name', title: '网站名称', size: 'medium' },
+        { name: 'describe', title: '网站描述', size: 'medium' },
+        { name: 'company', title: '单位', size: 'medium' },
+        { name: 'phone', title: '电话', size: 'medium' },
+        { name: 'address', title: '地址', size: 'medium' },
+        { name: 'mail', title: '邮箱', size: 'medium' },
+        { name: 'postcode', title: '邮编', size: 'medium' },
+        { name: 'record', title: '备案号', size: 'medium' }
+      ],
+      rules: {
+        name: [
+          { required: true, message: '请输入网站名称', trigger: 'blur' }
+        ],
+        describe: [
+          { required: true, message: '请输入网站描述', trigger: 'blur' }
+        ],
+        company: [
+          { required: true, message: '请输入网站单位', trigger: 'blur' }
+        ],
+        phone: [
+          { required: true, message: '请输入网站电话', trigger: 'blur' }
+        ],
+        address: [
+          { required: true, message: '请输入网站地址', trigger: 'blur' }
+        ],
+        mail: [
+          { required: true, message: '请输入网站邮箱', trigger: 'blur' }
+        ],
+        postcode: [
+          { required: true, message: '请输入网站邮编', trigger: 'blur' }
+        ],
+        record: [
+          { required: true, message: '请输入网站备案号', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['configurationquery', 'configurationcreate', 'configurationupdate']),
+    async save (e) {
+      console.log(e)
+      if (this.isNew) {
+        // 修改
+        const res = await this.configurationupdate(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          await this.configurationquery()
+        }
+      } else {
+        // 添加
+        const res = await this.configurationcreate(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          await this.configurationquery()
+        }
+      }
+    }
+  },
+  async mounted () {
+    const res = await this.configurationquery()
+    // eslint-disable-next-line eqeqeq
+    if (res.errcode == 0) {
+      this.$refs.ruleForm.reset()
+    }
+  },
+  computed: {
+    ...mapState(['configurationdata']),
+    isNew () {
+      return Boolean(this.configurationdata && this.configurationdata._id)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+  .main {
+    width: 60%;
+    margin: 0 auto;
+  }
+}
+</style>

+ 392 - 0
src/views/wokes/content/content.vue

@@ -0,0 +1,392 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>文章管理</span>
+        <el-button style="float: right; padding: 3px 0" type="text" @click="addcontent">添加文章</el-button>
+      </div>
+      <div class="main">
+        <el-tree
+          :data="menuList"
+          default-expand-all
+          :props="defaultProps"
+          @node-click="treeClick"
+          node-key="code"
+          ref="deeptree"
+          class="deeptree"
+        >
+        </el-tree>
+        <el-card class="maincard">
+          <div slot="header" class="clearfix">
+            <span>所属菜单:{{ is_title }}</span>
+          </div>
+            <naf-grid class="grid" ref="grid" @edit="edit" @delete="deletecontent" :data="contentList" :meta="meta" :total="total" @query="query"></naf-grid>
+        </el-card>
+      </div>
+    </el-card>
+    <el-card class="box-dj" v-if="visible">
+      <div slot="header" class="clearfix">
+        <span>{{ isNew ? '修改文章' : '添加文章' }}</span>
+      </div>
+      <div class="main">
+        <naf-form v-if="visible" ref="ruleForm" @save="save" :meta="formmeta" :rules="rules" :data="is_data" :close="true" @close="close">
+          <template v-slot:field="{ form, item }">
+            <!-- 图片上传 -->
+            <el-upload
+              v-if="item.name == 'thumbnail'"
+              class="avatar-uploader avatar"
+              action="/api/files/upload"
+              :show-file-list="false"
+              :on-success="handleAvatarSuccess"
+              :before-upload="beforeAvatarUpload"
+              :data="{ type: 'resource' }"
+              :headers="myHeaders"
+              :on-error="imgerror"
+            >
+              <img v-if="imageUrl" :src="imageUrl" class="avatar">
+              <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+            </el-upload>
+            <!-- 选择日期 -->
+            <el-date-picker
+              value-format="yyyy-MM-dd"
+              v-model="form[item.name]"
+              v-if="item.name == 'date'"
+              type="date"
+              placeholder="选择日期">
+            </el-date-picker>
+            <!-- 菜单选择 -->
+            <el-select v-if="item.name == 'menus'" v-model="form[item.name]" placeholder="请选择绑定菜单">
+              <el-option
+                v-for="item in menuList"
+                :key="item._id"
+                :label="item.name"
+                :value="item.code">
+              </el-option>
+            </el-select>
+            <!-- 附件上传 -->
+            <el-upload
+              v-if="item.name == 'annex'"
+              class="upload-demo"
+              action="/api/files/upload"
+              :limit="1"
+              :on-exceed="handleExceed"
+              :on-success="handleFilesSuccess"
+              :before-upload="beforeFilesUpload"
+              :data="{ type: 'files' }"
+              :headers="myHeaders"
+              :before-remove="beforefileremove"
+              :on-error="fileserror"
+              :file-list="fileList">
+              <el-button size="small" type="primary">点击上传</el-button>
+            </el-upload>
+            <!-- 富文本 -->
+            <editor-bar v-if="item.name == 'content'" v-model="form[item.name]" :isClear="isClear"></editor-bar>
+          </template>
+          <template v-slot:end="{ form, item }">
+            <el-form-item :label="item.title" v-if="item.name == 'term' && form.menus == '4'" :prop="item.name">
+              <el-input v-model="form[item.name]"></el-input>
+            </el-form-item>
+          </template>
+        </naf-form>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import nafForm from '@naf/data/form'
+import editorBar from '@naf/data/editoritem'
+import { createNamespacedHelpers, mapMutations } from 'vuex'
+const token = sessionStorage.getItem('token')
+const { mapState, mapActions } = createNamespacedHelpers('content')
+const { mapState: cmenusmapState, mapActions: menusmapActions } = createNamespacedHelpers('wokesmenu')
+export default {
+  components: {
+    nafGrid,
+    nafForm,
+    editorBar
+  },
+  data () {
+    return {
+      is_title: null,
+      isClear: false,
+      detail: '',
+      myHeaders: { Authorization: `Bearer ${token}` },
+      imageUrl: '',
+      fileList: [],
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'title', title: '标题', filter: true },
+        { name: 'date', title: '发表日期' }
+      ],
+      formmeta: [
+        { name: 'thumbnail', title: '缩略图', slots: 'field' },
+        { name: 'title', title: '标题' },
+        { name: 'date', title: '发表日期', slots: 'field' },
+        { name: 'menus', title: '绑定菜单', slots: 'field' },
+        { name: 'term', title: '文章期目', slots: 'end' },
+        { name: 'annex', title: '附件', slots: 'field' },
+        { name: 'content', title: '内容', slots: 'field' }
+      ],
+      rules: {
+        title: [
+          { required: true, message: '请输入标题', trigger: 'blur' }
+        ],
+        slug: [
+          { required: true, message: '请输入摘要', trigger: 'blur' }
+        ],
+        date: [
+          { required: true, message: '请选择日期', trigger: 'blur' }
+        ],
+        columns: [
+          { required: true, message: '请选择绑定菜单', trigger: 'blur' }
+        ],
+        thumbnail: [
+          { required: true, message: '请上传缩略图', trigger: 'blur' }
+        ],
+        content: [
+          { required: true, message: '请输入内容', trigger: 'blur' }
+        ],
+        term: [
+          { required: true, message: '请输入期目', trigger: 'blur' }
+        ]
+      },
+      defaultProps: {
+        children: 'children',
+        label: 'name'
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['contentquery', 'contentcreate', 'contentupdate', 'contentdelete', 'contentdetails']),
+    ...menusmapActions(['menuquery']),
+    ...mapMutations(['setcolumns']),
+    // 点击树
+    treeClick (data) {
+      this.is_title = data.name
+      this.data = data
+      this.query()
+    },
+    // 添加
+    async addcontent () {
+      this.is_data = {
+        menus: this.data.code
+      }
+      this.visible = true
+    },
+    // 删除
+    async deletecontent (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.contentdelete(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          this.query()
+          this.$refs.grid.resetpage(-1)
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 修改
+    async edit (e) {
+      // await this.columnquery({ filter: {}, paging: {} })
+      await this.contentdetails({ _id: e._id })
+      this.is_data = { ...this.contentItem }
+      if (this.is_data.annex) this.fileList.push({ name: this.is_data.annexname, url: this.is_data.annex })
+      if (this.contentItem.thumbnail) this.imageUrl = this.contentItem.thumbnail
+      this.visible = true
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      filter.code = this.data.code
+      await this.contentquery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      if (this.isNew) {
+        // 修改
+        res = await this.contentupdate(e)
+      } else {
+        // 添加
+        res = await this.contentcreate(e)
+      }
+      // eslint-disable-next-line eqeqeq
+      if (res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.query()
+        this.$refs.grid.resetpage(-1)
+        this.visible = false
+        this.fileList = []
+        this.imageUrl = ''
+      }
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+      this.fileList = []
+      this.imageUrl = ''
+    },
+    // 文件上传
+    // 文件列表移除文件时的钩子
+    beforefileremove (file, fileList) {
+      this.is_data.annex = null
+      this.$refs.ruleForm.form.annex = null
+      this.is_data.annexname = null
+      this.$refs.ruleForm.form.annexname = null
+    },
+    // 文件上传成功时的钩子
+    handleFilesSuccess (res, file) {
+      this.is_data = this.$refs.ruleForm.form
+      this.is_data.annex = res.data.path
+      this.is_data.annexname = res.data.name
+    },
+    // 文件列表移除文件时的钩子
+    beforeFilesUpload (file) {
+      const isType = file.type === 'image/jpeg' || 'image/png' || 'application/octet-stream' || 'application/zip' || 'application/octet-stream' || 'application/msword' || 'application/vnd.ms-excel' || 'application/x-zip-compressed' || 'application/pdf'
+      const isLt2M = file.size / 1024 / 1024 < 5
+      if (!isType) {
+        this.$message.error('请上传正确格式')
+      }
+      if (!isLt2M) {
+        this.$message.error('上传图片大小不能超过 5MB!')
+      }
+      return isType && isLt2M
+    },
+    // 文件超出个数限制时的钩子
+    handleExceed (files, fileList) {
+      this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
+    },
+    fileserror () {
+      this.$message.error('上传失败')
+    },
+    // 图片上传
+    // 文件上传成功时的钩子
+    handleAvatarSuccess (res, file) {
+      this.is_data = { ...this.$refs.ruleForm.form }
+      this.is_data.thumbnail = res.data.path
+      this.$refs.ruleForm.formChage('thumbnail', res.data.path)
+      this.imageUrl = URL.createObjectURL(file.raw)
+    },
+    imgerror () {
+      this.$message.error('上传失败')
+    },
+    // 上传文件之前的钩子
+    beforeAvatarUpload (file) {
+      const isJPG = file.type === 'image/jpeg'
+      const isPNG = file.type === 'image/png'
+      const isLt2M = file.size / 1024 / 1024 < 2
+
+      if (!isJPG && !isPNG) {
+        this.$message.error('上传图片只能是 JPG 或 PNG 格式!')
+      }
+      if (!isLt2M) {
+        this.$message.error('上传图片大小不能超过 2MB!')
+      }
+      return (isJPG || isPNG) && isLt2M
+    }
+  },
+  async mounted () {
+    const res = await this.menuquery({ filter: { type: 0 } })
+    // eslint-disable-next-line eqeqeq
+    if (res.errcode == 0) {
+      this.is_title = res.data[0].name
+      this.$refs.deeptree.setCurrentKey(res.data[0].code)
+      this.data = res.data[0]
+      this.query()
+      this.setcolumns(this.menuList)
+    }
+  },
+  computed: {
+    ...mapState(['total', 'contentList', 'contentItem']),
+    ...cmenusmapState(['menuList']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+  position: relative;
+  .box-card {
+    height: 100%;
+    /deep/ .el-card__body {
+      height: 100%;
+      padding: 0;
+    }
+  }
+}
+.main {
+  display: flex;
+  height: 100%;
+  .deeptree {
+    width: 15%;
+    height: 100%;
+    padding-top: 1%;
+  }
+  .maincard {
+    width: 85%;
+    height: 100%;
+    padding: 0;
+    margin: 0;
+    /deep/ .el-card__body {
+      height: 100%;
+    }
+  }
+}
+.box-dj {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  left: 0;
+  top: 0;
+  z-index: 999;
+  /deep/ .el-card__body {
+    height: 90%;
+    overflow-y: auto;
+    width: 100%;
+    .main {
+      width: 60%;
+      margin: 0 auto;
+      margin-bottom: 4em;
+    }
+  }
+}
+.avatar-uploader {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+}
+.avatar-uploader .el-upload:hover {
+  border-color: #409EFF;
+}
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 150px;
+  height: 150px;
+  line-height: 150px;
+  text-align: center;
+}
+.avatar {
+  width: 150px;
+  height: 150px;
+  display: block;
+}
+
+</style>

+ 93 - 0
src/views/wokes/content/files.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>文件管理</span>
+      </div>
+      <div class="main">
+        <naf-grid ref="grid" :operation="operation" @delete="deletefiles" @dwfile="dwfile" :data="filesList" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('files')
+export default {
+  components: {
+    nafGrid
+  },
+  data () {
+    return {
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'name', title: '文件名称', filter: true },
+        { name: 'path', title: '文件路径' },
+        { name: 'createAt', title: '上传时间' }
+      ],
+      operation: [
+        { name: 'dwfile', title: '下载', icons: 'el-icon-download' },
+        { name: 'delete', title: '删除', icons: 'el-icon-delete' }
+      ]
+    }
+  },
+  methods: {
+    ...mapActions(['filesquery', 'filesdelete']),
+    // 删除
+    async deletefiles (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.filesdelete(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          this.query()
+          this.$refs.grid.resetpage(-1)
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      filter.type = 'files'
+      await this.filesquery({ filter, paging })
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+    },
+    // 下载
+    dwfile (e) {
+      var a = document.createElement('a')
+      a.download = e.name
+      a.href = e.path
+      a.click()
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'filesList'])
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+}
+</style>

+ 198 - 0
src/views/wokes/content/menu.vue

@@ -0,0 +1,198 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>菜单管理</span>
+        <el-button style="float: right; padding: 3px 0" type="text" @click="adduser">添加菜单</el-button>
+      </div>
+      <div class="main">
+        <naf-grid ref="grid" @edit="edit" @delete="deleterole" :data="menuList" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+    <dialog-drawer type="dialog" :visible="visible" :title="isNew ? '修改菜单' : '添加菜单'" @close="close" :width="'30%'">
+      <template v-slot:content>
+        <naf-form class="ruleForm" v-if="visible" ref="ruleForm" @save="save" :meta="formmeta" :rules="rules" :data="is_data" :close="true" @close="close">
+          <template v-slot:end="{ form, item }">
+            <el-form-item :label="item.title" v-if="item.name == 'uri' && form.type == '1'" :prop="item.name">
+              <el-input v-model="form[item.name]"></el-input>
+            </el-form-item>
+            <!-- 单页选择 -->
+            <el-form-item :label="item.title" v-if="item.name == 'pages' && form.type == '2'" :prop="item.name">
+              <el-select v-model="form[item.name]" placeholder="请选择单页">
+                <el-option
+                  v-for="item in pageList"
+                  :key="item._id"
+                  :label="item.title"
+                  :value="item._id">
+                </el-option>
+              </el-select>
+            </el-form-item>
+          </template>
+        </naf-form>
+      </template>
+    </dialog-drawer>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import dialogDrawer from '@naf/data/dialog -drawer'
+import nafForm from '@naf/data/form'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('wokesmenu')
+const { mapState: pagesmapState, mapActions: pagesmapActions } = createNamespacedHelpers('pages')
+export default {
+  components: {
+    nafGrid,
+    dialogDrawer,
+    nafForm
+  },
+  data () {
+    return {
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'name', title: '菜单名称', filter: true },
+        { name: 'code', title: '菜单编码', filter: true },
+        { name: 'state', title: '状态', formatter: 'state' },
+        { name: 'type', title: '类型', formatter: 'type' },
+        { name: 'en', title: '英文缩写' },
+        { name: 'sort', title: '排序' }
+      ],
+      formmeta: [
+        { name: 'name', title: '菜单名称' },
+        { name: 'code', title: '菜单编码', disabled: false },
+        { name: 'state', title: '状态', formatter: 'state' },
+        { name: 'type', title: '类型', formatter: 'type', disabled: false },
+        { name: 'uri', title: '访问地址', slots: 'end' },
+        { name: 'pages', title: '绑定单页', slots: 'end' },
+        { name: 'en', title: '英文缩写' },
+        { name: 'sort', title: '排序', placeholder: '请输入数字 0 - 100' }
+      ],
+      rules: {
+        name: [
+          { required: true, message: '请输入菜单名称', trigger: 'blur' }
+        ],
+        code: [
+          { required: true, message: '请输入菜单编码', trigger: 'blur' }
+        ],
+        state: [
+          { required: true, message: '请选择菜单状态', trigger: 'blur' }
+        ],
+        type: [
+          { required: true, message: '请输入菜单类型', trigger: 'blur' }
+        ],
+        en: [
+          { required: true, message: '请输入菜单英文缩写', trigger: 'blur' }
+        ],
+        uri: [
+          { required: true, message: '请输入访问地址', trigger: 'blur' }
+        ],
+        pages: [
+          { required: true, message: '请选择单页', trigger: 'blur' }
+        ],
+        sort: [
+          { required: true, message: '请输入排序', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['menuquery', 'menucreate', 'menuupdate', 'menudelete']),
+    ...pagesmapActions(['pagequery']),
+    // 添加
+    adduser () {
+      this.is_data = {}
+      this.visible = true
+      // eslint-disable-next-line no-return-assign
+      this.formmeta.map(p => p.name === 'code' ? p.disabled = false : true)
+      // eslint-disable-next-line no-return-assign
+      this.formmeta.map(p => p.name === 'type' ? p.disabled = false : true)
+    },
+    // 删除
+    async deleterole (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.menudelete(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          this.query()
+          this.$refs.grid.resetpage(-1)
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 修改
+    edit (e) {
+      this.is_data = e
+      this.visible = true
+      // eslint-disable-next-line no-return-assign
+      this.formmeta.map(p => p.name === 'code' ? p.disabled = true : false)
+      // eslint-disable-next-line no-return-assign
+      this.formmeta.map(p => p.name === 'type' ? p.disabled = true : false)
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.menuquery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      // eslint-disable-next-line eqeqeq
+      if (Number(e.sort) < 0 || Number(e.sort) == 'NaN') {
+        this.$message.error('请输入0-100 数字')
+      }
+      if (this.isNew) {
+        // 修改
+        res = await this.menuupdate(e)
+      } else {
+        // 添加
+        res = await this.menucreate(e)
+      }
+      // eslint-disable-next-line eqeqeq
+      if (res && res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.query()
+        this.$refs.grid.resetpage(-1)
+        this.visible = false
+      }
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+    }
+  },
+  async mounted () {
+    this.query()
+    await this.pagequery()
+  },
+  computed: {
+    ...mapState(['total', 'menuList']),
+    ...pagesmapState(['pageList']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+}
+// .ruleForm {
+//   width: 50%;
+//   margin: 0 auto;
+// }
+</style>

+ 266 - 0
src/views/wokes/content/pages.vue

@@ -0,0 +1,266 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>单页管理</span>
+        <el-button style="float: right; padding: 3px 0" type="text" @click="addpage">添加单页</el-button>
+      </div>
+      <div class="main">
+        <naf-grid ref="grid" @edit="edit" @delete="deletepage" :data="items" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+    <el-card class="box-dj" v-if="visible">
+      <div slot="header" class="clearfix">
+        <span>{{ isNew ? '修改单页' : '添加单页' }}</span>
+      </div>
+      <div class="main">
+        <naf-form v-if="visible" ref="ruleForm" @save="save" :meta="formmeta" :rules="rules" :data="is_data" :close="true" @close="close">
+          <template v-slot:field="{ form, item }">
+            <!-- 选择菜单 -->
+            <!-- <el-select v-if="item.name == 'menu'" v-model="form[item.name]" placeholder="请选择绑定菜单">
+              <el-option
+                v-for="item in menuList"
+                :key="item._id"
+                :label="item.name"
+                :value="item.code">
+              </el-option>
+            </el-select> -->
+            <!-- 选择日期 -->
+            <el-date-picker
+              value-format="yyyy-MM-dd"
+              v-model="form[item.name]"
+              v-if="item.name == 'date'"
+              type="date"
+              placeholder="选择日期">
+            </el-date-picker>
+            <!-- 附件上传 -->
+            <el-upload
+              v-if="item.name == 'annex'"
+              class="upload-demo"
+              action="/api/files/upload"
+              :limit="1"
+              :on-exceed="handleExceed"
+              :on-success="handleFilesSuccess"
+              :before-upload="beforeFilesUpload"
+              :data="{ type: 'files' }"
+              :headers="myHeaders"
+              :before-remove="beforefileremove"
+              :file-list="fileList">
+              <el-button size="small" type="primary">点击上传</el-button>
+            </el-upload>
+            <!-- 富文本 -->
+            <editor-bar v-if="item.name == 'content'" v-model="form[item.name]" :isClear="isClear"></editor-bar>
+          </template>
+        </naf-form>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import nafForm from '@naf/data/form'
+import editorBar from '@naf/data/editoritem'
+import { createNamespacedHelpers } from 'vuex'
+const token = sessionStorage.getItem('token')
+const { mapState, mapActions } = createNamespacedHelpers('pages')
+const { mapState: menumapState, mapActions: menumapActions } = createNamespacedHelpers('wokesmenu')
+export default {
+  components: {
+    nafGrid,
+    nafForm,
+    editorBar
+  },
+  data () {
+    return {
+      isClear: false,
+      detail: '',
+      myHeaders: { Authorization: `Bearer ${token}` },
+      imageUrl: '',
+      fileList: [],
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'menu', title: '所属菜单' },
+        { name: 'title', title: '标题', filter: true },
+        { name: 'date', title: '发表日期' }
+      ],
+      formmeta: [
+        { name: 'title', title: '标题' },
+        { name: 'date', title: '发表日期', slots: 'field' },
+        // { name: 'menu', title: '绑定菜单', slots: 'field' },
+        { name: 'annex', title: '附件', slots: 'field' },
+        { name: 'content', title: '内容', slots: 'field' }
+      ],
+      rules: {
+        title: [
+          { required: true, message: '请输入标题', trigger: 'blur' }
+        ],
+        menu: [
+          { required: true, message: '请选择绑定菜单', trigger: 'blur' }
+        ],
+        content: [
+          { required: true, message: '请输入内容', trigger: 'blur' }
+        ],
+        date: [
+          { required: true, message: '请选择日期', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['pagequery', 'pagecreate', 'pageupdate', 'pagedelete', 'pagedetails']),
+    ...menumapActions(['menuquery']),
+    // 添加
+    async addpage () {
+      this.is_data = {}
+      this.visible = true
+    },
+    // 删除
+    async deletepage (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.pagedelete(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          this.query()
+          this.$refs.grid.resetpage(-1)
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 修改
+    async edit (e) {
+      // await this.menuquery({ filter: { type: '2' }, paging: {} })
+      await this.pagedetails({ _id: e._id })
+      this.is_data = { ...this.pageItem }
+      if (this.is_data.annex) {
+        this.fileList.push({ name: this.is_data.annexname, url: this.is_data.annex })
+      }
+      this.visible = true
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.pagequery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      if (this.isNew) {
+        // 修改
+        res = await this.pageupdate(e)
+      } else {
+        // 添加
+        res = await this.pagecreate(e)
+      }
+      // eslint-disable-next-line eqeqeq
+      if (res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.query()
+        this.$refs.grid.resetpage(-1)
+        this.visible = false
+        this.fileList = []
+        this.imageUrl = ''
+      }
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+      this.fileList = []
+      this.imageUrl = ''
+    },
+    // 文件上传
+    // 文件列表移除文件时的钩子
+    beforefileremove (file, fileList) {
+      this.is_data.annex = null
+      this.$refs.ruleForm.form.annex = null
+      this.is_data.annexname = null
+      this.$refs.ruleForm.form.annexname = null
+    },
+    // 文件上传成功时的钩子
+    handleFilesSuccess (res, file) {
+      this.is_data = this.$refs.ruleForm.form
+      this.is_data.annex = res.data.path
+      this.is_data.annexname = res.data.name
+    },
+    // 文件列表移除文件时的钩子
+    beforeFilesUpload (file) {
+      const isType = file.type === 'image/jpeg' || 'image/png' || 'application/octet-stream' || 'application/zip' || 'application/octet-stream' || 'application/msword' || 'application/vnd.ms-excel' || 'application/x-zip-compressed' || 'application/pdf'
+      const isLt2M = file.size / 1024 / 1024 < 5
+      if (!isType) {
+        this.$message.error('请上传正确格式')
+      }
+      if (!isLt2M) {
+        this.$message.error('上传图片大小不能超过 5MB!')
+      }
+      return isType && isLt2M
+    },
+    // 文件超出个数限制时的钩子
+    handleExceed (files, fileList) {
+      this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
+    }
+  },
+  async mounted () {
+    this.query()
+    await this.menuquery({ filter: { type: '2' }, paging: {} })
+  },
+  computed: {
+    ...mapState(['total', 'pageList', 'pageItem']),
+    ...menumapState(['menuList']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    },
+    items () {
+      return this.pageList.map(p => {
+        const name = this.menuList.filter(e => e.pages === p._id)
+        if (name.length > 0) {
+          name.filter(e => {
+            if (p.menu && p.menu.indexOf(e.name) === -1) {
+              p.menu += `,${e.name}`
+            } else {
+              p.menu = `${e.name}`
+            }
+          })
+        }
+        return p
+      })
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+  position: relative;
+}
+.box-dj {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  left: 0;
+  top: 0;
+  z-index: 999;
+  /deep/ .el-card__body {
+    height: 100%;
+    overflow-y: auto;
+    width: 100%;
+    .main {
+      width: 60%;
+      margin: 0 auto;
+      margin-bottom: 4em;
+    }
+  }
+}
+.box-card {
+  height: 100%;
+}
+</style>

+ 93 - 0
src/views/wokes/content/resource.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>资源管理</span>
+      </div>
+      <div class="main">
+        <naf-grid ref="grid" :operation="operation" @delete="deletefiles" @dwfile="dwfile" :data="filesList" :meta="meta" :total="total" @query="query"></naf-grid>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('files')
+export default {
+  components: {
+    nafGrid
+  },
+  data () {
+    return {
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'name', title: '资源名称', filter: true },
+        { name: 'path', title: '资源路径' },
+        { name: 'createAt', title: '上传时间' }
+      ],
+      operation: [
+        { name: 'dwfile', title: '下载', icons: 'el-icon-download' },
+        { name: 'delete', title: '删除', icons: 'el-icon-delete' }
+      ]
+    }
+  },
+  methods: {
+    ...mapActions(['filesquery', 'filesdelete']),
+    // 删除
+    async deletefiles (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.filesdelete(e)
+        // eslint-disable-next-line eqeqeq
+        if (res.errcode == 0) {
+          this.$message.success('操作成功')
+          this.query()
+          this.$refs.grid.resetpage(-1)
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        })
+      })
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      filter.type = 'resource'
+      await this.filesquery({ filter, paging })
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+    },
+    // 下载
+    dwfile (e) {
+      var a = document.createElement('a')
+      a.download = e.name
+      a.href = e.path
+      a.click()
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'filesList'])
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+  height: 100%;
+}
+.box-card {
+  height: 100%;
+}
+</style>

+ 32 - 0
vue.config.js

@@ -0,0 +1,32 @@
+const path = require('path')
+const baseUrl = '/admin'
+
+const frameSrc = path.resolve(__dirname)
+module.exports = {
+  publicPath: baseUrl,
+  productionSourceMap: false,
+  configureWebpack: {
+    resolve: {
+      alias: {
+        '@frame': frameSrc,
+        '@naf': path.join(frameSrc, '/naf'),
+        '@lib': path.join(frameSrc, '/lib')
+      }
+    }
+  },
+  devServer: {
+    port: 4000,
+    proxy: {
+      '/api/': {
+        target: 'http://localhost:7001'
+        // target: 'http://192.168.4.1:7001'
+      },
+      '/public/': {
+        target: 'http://localhost:7001'
+      },
+      '/files/images/': {
+        target: 'http://jjzh.cc-lotus.info'
+      }
+    }
+  }
+}