asd123a20 3 éve
szülő
commit
e0814a893e
70 módosított fájl, 18231 hozzáadás és 0 törlés
  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. 21 0
      lib/plug/setting.js
  11. 45 0
      lib/plug/stomp.js
  12. 68 0
      lib/style/index.less
  13. 109 0
      naf/data/deep-tree.vue
  14. 24 0
      naf/data/dialog -drawer.md
  15. 68 0
      naf/data/dialog -drawer.vue
  16. 168 0
      naf/data/editoritem.vue
  17. 19 0
      naf/data/form.md
  18. 80 0
      naf/data/form.vue
  19. 56 0
      naf/data/tables/grid.md
  20. 78 0
      naf/data/tables/naf-grid.vue
  21. 54 0
      naf/data/tables/pagination .vue
  22. 70 0
      naf/data/tables/search.vue
  23. 79 0
      naf/data/tables/table.vue
  24. 54 0
      naf/layout/breadcrumb.vue
  25. 34 0
      naf/layout/menu-item.vue
  26. 69 0
      naf/layout/menu.vue
  27. 105 0
      naf/layout/user.vue
  28. 12989 0
      package-lock.json
  29. 41 0
      package.json
  30. BIN
      public/favicon.ico
  31. 17 0
      public/index.html
  32. 40 0
      public/worker/token.js
  33. 7 0
      src/App.vue
  34. BIN
      src/assets/bg2.jpg
  35. BIN
      src/assets/home.png
  36. 1 0
      src/assets/logo1.svg
  37. 60 0
      src/components/HelloWorld.vue
  38. 21 0
      src/main.js
  39. 14 0
      src/router/gaf.js
  40. 45 0
      src/router/index.js
  41. 27 0
      src/router/wokes.js
  42. 55 0
      src/store/gaf/adminuser.js
  43. 74 0
      src/store/gaf/code.js
  44. 35 0
      src/store/gaf/log.js
  45. 35 0
      src/store/gaf/login.js
  46. 110 0
      src/store/gaf/menu.js
  47. 50 0
      src/store/gaf/role.js
  48. 55 0
      src/store/index.js
  49. 60 0
      src/store/wokes/content.js
  50. 38 0
      src/store/wokes/files.js
  51. 60 0
      src/store/wokes/hospital.js
  52. 60 0
      src/store/wokes/order.js
  53. 60 0
      src/store/wokes/pages.js
  54. 60 0
      src/store/wokes/specialist.js
  55. 60 0
      src/store/wokes/subject.js
  56. 48 0
      src/views/frame/Home.vue
  57. 203 0
      src/views/frame/Login.vue
  58. 64 0
      src/views/frame/demo.vue
  59. 143 0
      src/views/frame/frame.vue
  60. 142 0
      src/views/frame/router-frame.vue
  61. 205 0
      src/views/gaf/code.vue
  62. 57 0
      src/views/gaf/log.vue
  63. 185 0
      src/views/gaf/user.vue
  64. 308 0
      src/views/wokes/content/content.vue
  65. 206 0
      src/views/wokes/content/pages.vue
  66. 318 0
      src/views/wokes/hospital/hospital.vue
  67. 353 0
      src/views/wokes/hospital/specialist.vue
  68. 316 0
      src/views/wokes/hospital/subject.vue
  69. 213 0
      src/views/wokes/order.vue
  70. 43 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

+ 21 - 0
lib/plug/setting.js

@@ -0,0 +1,21 @@
+import Vue from 'vue'
+
+Vue.config.stomp = {
+  host: `wss://${location.host}/ws`,
+  // host: 'wss://172.17.116.7:15674/', // ws://${location.host}/ws
+  connectHeaders: {
+    host: 'smart',
+    login: 'smart',
+    passcode: 'smart123'
+  },
+  reconnectDelay: 5000,
+  heartbeatIncoming: 4000,
+  heartbeatOutgoing: 4000,
+  debug: false
+}
+
+Vue.config.weixin = {
+  // baseUrl: process.env.BASE_URL + 'weixin',
+  baseUrl: `http://${location.host}`
+  // baseUrl: 'http://192.168.0.81:8001'
+}

+ 45 - 0
lib/plug/stomp.js

@@ -0,0 +1,45 @@
+
+// import Vue from 'vue'
+import _ from 'lodash'
+import Stomp from 'stompjs'
+
+const Plugin = {
+  install (vue, options) {
+    // 4. 添加实例方法
+    vue.prototype.$stomp = function (subscribes = {}) {
+      let msg
+      // 获得Stomp client对象
+      const ws = new WebSocket(options.host)
+      const client = Stomp.over(ws)
+
+      // 成功回调
+      const connectCallback = function (x) {
+        // 返回结果
+        // '/exchange/qrcode.topic/qrcode'
+        Object.keys(subscribes)
+          .filter(p => _.isFunction(subscribes[p]))
+          .forEach(key => {
+            client.subscribe(key, subscribes[key])
+          })
+      }
+
+      // 失败回调
+      const errorCallback = function (error) {
+        msg = error
+      }
+      client.heartbeat.outgoing = options.heartbeatOutgoing
+      client.heartbeat.incoming = options.heartbeatIncoming
+      // client.reconnect.delay = options.reconnectDelay
+      // 添加参数  回调函数
+      client.connect(options.connectHeaders, connectCallback, errorCallback)
+      if (options.debug) {
+        client.debug = function (str) {
+          console.log(str, 'debug')
+        }
+      }
+      return msg
+    }
+  }
+}
+
+export default Plugin

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

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

@@ -0,0 +1,109 @@
+<template>
+  <div class="treeContainer">
+    <el-input size="mini" class="filter" v-if="treeFilter" placeholder="输入关键字进行搜索" v-model="filterText"></el-input>
+    <el-tree
+      node-key="code"
+      ref="deeptree"
+      default-expand-all
+      :data="datas"
+      :filter-node-method="filterNode"
+      @node-click="treeClick"
+      :render-content="renderContent"
+      :expand-on-click-node="false"
+    >
+    </el-tree>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    // 形成树的扁平化数据  关联方式parentId
+    data: { type: Array },
+    // 是否启用搜索
+    treeFilter: { type: Boolean, default: false },
+    // 是否启用操作
+    operation: { 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) {
+      this.$emit('treeclick', data)
+    },
+    // 修改
+    edit (data) {
+      this.$emit('edit', data)
+    },
+    // 删除
+    remove (node, data) {
+      this.$emit('delete', data)
+    },
+    // 操作按钮
+    renderContent (h, { node, data, store }) {
+      let htm = (<span class="custom-tree-node">
+        <span>{data.name}</span>
+      </span>)
+      if (this.operation) {
+        htm = (
+          <span class="custom-tree-node">
+            <span class="title">{data.name}</span>
+            <span class="node">
+              <el-button size="mini" icon="el-icon-edit" type="text" on-click={ () => this.edit(data) }></el-button>
+              <el-button size="mini" icon="el-icon-delete" type="text" on-click={ () => this.remove(node, data) }></el-button>
+            </span>
+          </span>)
+      }
+      return htm
+    }
+  },
+  mounted () {},
+  watch: {
+    filterText (val) {
+      this.$refs.deeptree.filter(val)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.treeContainer {
+  width: 100%;
+  height: 100%;
+  // border-right: 1px solid #dadada;
+  .filter {
+    width: 90%;
+    margin: 20px auto;
+    display: block;
+  }
+  /deep/ .el-tree-node__content {
+    width: 100%;
+    .custom-tree-node {
+      width: 100%;
+      .title {
+        float: left;
+      }
+      .node {
+        float: right;
+      }
+    }
+  }
+}
+</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>

+ 168 - 0
naf/data/editoritem.vue

@@ -0,0 +1,168 @@
+<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 = '/tyyl/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.uri
+        console.log(url)
+        // 上传图片,返回结果,将图片插入到编辑器中
+        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.uri
+      //     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.name" :value="i.code"></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.formatter) || []
+          // eslint-disable-next-line eqeqeq
+          const item = dict.filter(z => p[i.name] == z.code)
+          if (item.length > 0) {
+            p[i.name] = item[0].name
+          }
+        })
+      })
+      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>

+ 105 - 0
naf/layout/user.vue

@@ -0,0 +1,105 @@
+<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 command="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.oldpass"></el-input>
+          </el-form-item>
+          <el-form-item label="新密码" prop="newpassword">
+            <el-input v-model="form.newpass"></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 { createNamespacedHelpers } from 'vuex'
+const { mapActions } = createNamespacedHelpers('adminuser')
+export default {
+  components: {
+    dialogDrawer
+  },
+  data () {
+    return {
+      menuitem: '',
+      circleUrl: null,
+      form: {},
+      visible: false,
+      rules: {
+        oldpass: [
+          { required: true, message: '请输入原密码', trigger: 'blur' }
+        ],
+        newpass: [
+          { required: true, message: '请输入新密码', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  computed: {
+    userName () {
+      const userName = sessionStorage.getItem('userName') || ''
+      return userName
+    }
+  },
+  methods: {
+    ...mapActions(['editPwa']),
+    handleCommand (command) {
+      console.log(command, 'command')
+      if (command === 'pwd') this.visible = true
+      if (command === 'clocs') {
+        sessionStorage.removeItem('token')
+        sessionStorage.removeItem('userName')
+        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>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 12989 - 0
package-lock.json


+ 41 - 0
package.json

@@ -0,0 +1,41 @@
+{
+  "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",
+    "sockjs-client": "^1.5.2",
+    "stompjs": "^2.3.3",
+    "uuidjs": "^4.2.9",
+    "vue": "^2.6.11",
+    "vue-qr": "^2.5.0",
+    "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>

+ 40 - 0
public/worker/token.js

@@ -0,0 +1,40 @@
+/*
+ * @Author: your name
+ * @Date: 2020-05-28 10:28:16
+ * @LastEditTime: 2020-05-29 11:15:12
+ * @LastEditors: Please set LastEditors
+ * @Description: In User Settings Edit
+ * @FilePath: \admin-frame\public\worker\worker.js
+ */
+let time
+onmessage = function (e) {
+  const data = e.data
+  time = e.data.time / 1000 - 6
+  setInterval(jwtTime, 6000, data)
+}
+
+function jwtTime ({ token }) {
+  var base64Url = token.split('.')[1]
+  var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
+  var jsonPayload = decodeURIComponent(
+    atob(base64)
+      .split('')
+      .map(function (c) {
+        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
+      })
+      .join('')
+  )
+  const exp = JSON.parse(jsonPayload).exp
+  time = Number(time + 6)
+  if (time > exp) {
+    postMessage({
+      errcode: -1,
+      errmsg: '登录过期'
+    })
+  } else {
+    postMessage({
+      errcode: 0,
+      errmsg: '未过期'
+    })
+  }
+}

+ 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


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 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>

+ 21 - 0
src/main.js

@@ -0,0 +1,21 @@
+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'
+import Plugin from '@lib/plug/stomp'
+import '@lib/plug/setting'
+Vue.config.productionTip = false
+Vue.use(ElementUI)
+Vue.use(deepTree)
+Vue.use(dict)
+Vue.use(Plugin, Vue.config.stomp)
+new Vue({
+  router,
+  store,
+  render: h => h(App)
+}).$mount('#app')

+ 14 - 0
src/router/gaf.js

@@ -0,0 +1,14 @@
+export default [
+  {
+    path: '/frame/gaf/user',
+    component: () => import('../views/gaf/user.vue')
+  },
+  {
+    path: '/frame/gaf/log',
+    component: () => import('../views/gaf/log.vue')
+  },
+  {
+    path: '/frame/gaf/code',
+    component: () => import('../views/gaf/code.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

+ 27 - 0
src/router/wokes.js

@@ -0,0 +1,27 @@
+export default [
+  // 以下是内容管理
+  {
+    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/hospital',
+    component: () => import('../views/wokes/hospital/hospital.vue')
+  },
+  {
+    path: '/frame/wokes/subject',
+    component: () => import('../views/wokes/hospital/subject.vue')
+  },
+  {
+    path: '/frame/wokes/specialist',
+    component: () => import('../views/wokes/hospital/specialist.vue')
+  },
+  {
+    path: '/frame/wokes/order',
+    component: () => import('../views/wokes/order.vue')
+  }
+]

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

@@ -0,0 +1,55 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  adminUser: '/api/admin/query',
+  usercreate: '/api/admin/create',
+  userupdate: '/api/admin/update',
+  userdelete: '/api/admin/delete/',
+  editPwa: '/api/editPwa'
+}
+
+// 参数帐号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
+  },
+  async editPwa ({ commit }, payload) {
+    const res = await axios.post(api.editPwa, payload)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  adminUser (state, payload) {
+    state.userList = payload.data
+    state.total = payload.total
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

+ 74 - 0
src/store/gaf/code.js

@@ -0,0 +1,74 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  codelist: '/naf/items/',
+  typecreate: '/naf/category/create',
+  querytype: '/naf/category/list',
+  typeupdate: '/naf/category/update',
+  typedelete: '/naf/category/delete'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  codeList: [],
+  total: 0,
+  treedata: []
+})
+
+// actions
+const actions = {
+  async querytype ({ commit }) {
+    const res = await axios.get(api.querytype)
+    if (res.errcode === 0) commit('querytype', res)
+    return res
+  },
+  async typecreate ({ commit }, payload) {
+    const res = await axios.post(api.typecreate, payload)
+    return res
+  },
+  async typeupdate ({ commit }, payload) {
+    const res = await axios.post(api.typeupdate, payload)
+    return res
+  },
+  async typedelete ({ commit }, { code }) {
+    const res = await axios.get(api.typedelete, { params: { code } })
+    return res
+  },
+  async codequery ({ commit }, { filter, paging, category } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(`${api.codelist}${category}/list`, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('codelist', res)
+    return res
+  },
+  async codecreate ({ commit }, payload) {
+    const res = await axios.post(`${api.codelist}${payload.category}/create`, payload)
+    return res
+  },
+  async codeupdate ({ commit }, payload) {
+    const res = await axios.post(`${api.codelist}${payload.category}/update`, payload)
+    return res
+  },
+  async codedelete ({ commit }, payload) {
+    const res = await axios.get(`${api.codelist}${payload.category}/delete`, { params: { ...payload } })
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  querytype (state, payload) {
+    // eslint-disable-next-line no-return-assign
+    payload.data.map(p => p.parentId = '')
+    state.treedata = payload.data
+  },
+  codelist (state, payload) {
+    state.codeList = 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
+}

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

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

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

@@ -0,0 +1,110 @@
+export default [
+  // 信息发布
+  {
+    title: '信息发布',
+    path: '1',
+    id: '1',
+    icons: 'el-icon-reading',
+    parentId: '',
+    module: '@wokes'
+  },
+  {
+    title: '单页管理',
+    path: '/wokes/pages',
+    id: '1-1',
+    icons: 'el-icon-document',
+    parentId: '1',
+    module: '@wokes'
+  },
+  {
+    title: '内容管理',
+    path: '/wokes/content',
+    id: '1-2',
+    icons: 'el-icon-tickets',
+    parentId: '1',
+    module: '@wokes'
+  },
+  // 预约服务
+  {
+    title: '预约服务',
+    path: '2',
+    id: '2',
+    icons: 'el-icon-document-add',
+    parentId: '',
+    module: '@wokes'
+  },
+  {
+    title: '医院管理',
+    path: '/wokes/hospital',
+    id: '2-1',
+    icons: 'el-icon-office-building',
+    parentId: '2',
+    module: '@wokes'
+  },
+  {
+    title: '科室管理',
+    path: '/wokes/subject',
+    id: '2-2',
+    icons: 'el-icon-house',
+    parentId: '2',
+    module: '@wokes'
+  },
+  {
+    title: '专家管理',
+    path: '/wokes/specialist',
+    id: '2-3',
+    icons: 'el-icon-user-solid',
+    parentId: '2',
+    module: '@wokes'
+  },
+  // 订单管理
+  {
+    title: '订单管理',
+    path: '3',
+    id: '3',
+    icons: 'el-icon-s-order',
+    parentId: '',
+    module: '@wokes'
+  },
+  {
+    title: '订单管理',
+    path: '/wokes/order',
+    id: '3-1',
+    icons: 'el-icon-s-order',
+    parentId: '3',
+    module: '@wokes'
+  },
+  // 系统管理
+  {
+    title: '系统管理',
+    path: '4',
+    id: '4',
+    icons: 'el-icon-s-operation',
+    parentId: '',
+    module: '@gaf'
+  },
+  {
+    title: '管理员管理',
+    path: '/gaf/user',
+    id: '4-1',
+    icons: 'el-icon-user',
+    parentId: '4',
+    module: '@gaf'
+  },
+  {
+    title: '字典管理',
+    path: '/gaf/code',
+    id: '4-3',
+    icons: 'el-icon-document-copy',
+    parentId: '4',
+    module: '@gaf'
+  },
+  {
+    title: '日志管理',
+    path: '/gaf/log',
+    id: '4-2',
+    icons: 'el-icon-s-order',
+    parentId: '4',
+    module: '@gaf'
+  }
+]

+ 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
+}

+ 55 - 0
src/store/index.js

@@ -0,0 +1,55 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import axios from '@lib/plug/axios.js'
+// 系统
+import adminuser from './gaf/adminuser'
+import login from './gaf/login'
+import log from './gaf/log'
+import code from './gaf/code'
+// 内容
+import pages from './wokes/pages'
+import content from './wokes/content'
+// 文件
+import files from './wokes/files'
+// 医院
+import hospital from './wokes/hospital'
+import subject from './wokes/subject'
+import specialist from './wokes/specialist'
+// 订单
+import order from './wokes/order'
+Vue.use(Vuex)
+const codes = ['columns', 'status', 'region', 'orderStatus']
+const uri = '/naf/items/'
+export default new Vuex.Store({
+  state: {
+    dict: {}
+  },
+  mutations: {
+    setdice (state, { type, list }) {
+      state.dict[type] = list
+    }
+  },
+  actions: {
+    init ({ commit }) {
+      codes.filter(async e => {
+        const res = await axios.get(`${uri}${e}/list`)
+        if (res.errcode === 0) {
+          commit('setdice', { type: e, list: res.data })
+        }
+      })
+    }
+  },
+  modules: {
+    adminuser,
+    login,
+    log,
+    code,
+    pages,
+    content,
+    files,
+    hospital,
+    subject,
+    specialist,
+    order
+  }
+})

+ 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/fetch/'
+}
+
+// 参数帐号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
+}

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

@@ -0,0 +1,38 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  filesupload: '/files/upload'
+}
+
+// 参数帐号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
+  }
+}
+
+// mutations
+const mutations = {
+  files (state, payload) {
+    state.filesList = payload.data
+    state.total = payload.total
+  }
+}
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+}

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

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

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

@@ -0,0 +1,60 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  orderquery: '/api/order/query',
+  ordercreate: '/api/order/create',
+  orderupdate: '/api/order/update',
+  orderdelete: '/api/order/delete/',
+  orderdetails: '/api/order/fetch/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  orderList: [],
+  total: 0,
+  orderItem: {}
+})
+
+// actions
+const actions = {
+  async orderquery ({ commit }, { filter = {}, paging = {} } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.orderquery, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('order', res)
+    return res
+  },
+  async ordercreate ({ commit }, payload) {
+    const res = await axios.post(api.ordercreate, payload)
+    return res
+  },
+  async orderupdate ({ commit }, payload) {
+    const res = await axios.post(api.orderupdate, payload)
+    return res
+  },
+  async orderdelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.orderdelete}${_id}`)
+    return res
+  },
+  async orderdetails ({ commit }, { _id }) {
+    const res = await axios.get(`${api.orderdetails}${_id}`)
+    if (res.errcode === 0) commit('details', res)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  order (state, payload) {
+    state.orderList = payload.data
+    state.total = payload.total
+  },
+  details (state, payload) {
+    state.orderItem = payload.data
+  }
+}
+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/pages/query',
+  pagecreate: '/api/pages/create',
+  pageupdate: '/api/pages/update',
+  pagedelete: '/api/pages/delete/',
+  pagedetails: '/api/pages/fetch/'
+}
+
+// 参数帐号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 }, { code }) {
+    const res = await axios.get(`${api.pagedetails}${code}`)
+    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
+}

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

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

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

@@ -0,0 +1,60 @@
+import axios from '@lib/plug/axios.js'
+const api = {
+  subjectquery: '/api/subject/query',
+  subjectcreate: '/api/subject/create',
+  subjectupdate: '/api/subject/update',
+  subjectdelete: '/api/subject/delete/',
+  subjectdetails: '/api/subject/fetch/'
+}
+
+// 参数帐号id  appid
+// initial state
+const state = () => ({
+  subjectList: [],
+  total: 0,
+  subjectItem: {}
+})
+
+// actions
+const actions = {
+  async subjectquery ({ commit }, { filter, paging } = {}) {
+    const { page = 1, size = 10 } = paging
+    const res = await axios.get(api.subjectquery, { params: { skip: page - 1 < 0 ? 0 : page - 1, limit: size, ...filter } })
+    if (res.errcode === 0) commit('subject', res)
+    return res
+  },
+  async subjectcreate ({ commit }, payload) {
+    const res = await axios.post(api.subjectcreate, payload)
+    return res
+  },
+  async subjectupdate ({ commit }, payload) {
+    const res = await axios.post(api.subjectupdate, payload)
+    return res
+  },
+  async subjectdelete ({ commit }, { _id }) {
+    const res = await axios.delete(`${api.subjectdelete}${_id}`)
+    return res
+  },
+  async subjectdetails ({ commit }, { _id }) {
+    const res = await axios.get(`${api.subjectdetails}${_id}`)
+    if (res.errcode === 0) commit('details', res)
+    return res
+  }
+}
+
+// mutations
+const mutations = {
+  subject (state, payload) {
+    state.subjectList = payload.data
+    state.total = payload.total
+  },
+  details (state, payload) {
+    state.subjectItem = 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>

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

@@ -0,0 +1,203 @@
+<template>
+  <div class="container">
+    <div class="min">
+      <div class="form">
+        <h1>{{ title }}</h1>
+        <el-form :model="loginForm" :rules="rules" ref="loginForm" v-if="tabs == 'userName'">
+          <el-form-item prop="userName">
+            <el-input v-model="loginForm.userName" 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 prop="code">
+            <el-input
+              class="codeBox"
+              v-model="loginForm.code"
+              placeholder="验证码"
+              prefix-icon="naf-icons naf-icon-verifycode"
+              @keyup.13.native="submitForm('loginForm')"
+            >
+              <img class="code" slot="append" :src="codeUrl" @click="codeLoad" />
+            </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>
+        <vue-qr v-else class="qrshow" :text="downloadData.url" style="display: block;" colorDark="#f67b29" colorLight="#fff" :logoSrc="downloadData.icon" :logoScale="0.3" :size="200"></vue-qr>
+        <div class="tabs">
+          <div class="userName" v-if="tabs !== 'userName'" @click="tabs = 'userName'">帐号密码登录</div>
+          <div class="qrcode" v-else @click="qr">微信扫码登录</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue'
+import UUID from 'uuidjs'
+import vueQr from 'vue-qr'
+import { createNamespacedHelpers } from 'vuex'
+const { mapActions } = createNamespacedHelpers('login')
+const title = process.env.VUE_APP_MENU_TITLE
+export default {
+  components: {
+    vueQr
+  },
+  data () {
+    return {
+      codeUrl: null,
+      uuid: null,
+      downloadData: {
+        url: ''
+      },
+      tabs: 'userName',
+      title,
+      loginForm: {
+        userName: '',
+        password: ''
+      },
+      rules: {
+        userName: [{ required: true, message: '请输入帐号' }],
+        password: [{ required: true, message: '请输入密码' }]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['login', 'getuuid', 'qrcodeToken']),
+    // 绑定微信
+    async qr () {
+      this.tabs = 'qrcode'
+      const res = await this.getuuid()
+      this.uuid = res.uuid
+      if (res.errcode === 0) {
+        this.downloadData = {
+          url: `${Vue.config.weixin.baseUrl}/api/qrcodelogin/${res.uuid}`,
+          icon: ''
+        }
+      }
+      this.$stomp({ '/exchange/qrcode.topic/login': this.onMessage })
+    },
+    async onMessage (msg) {
+      if (msg.body === 'success') {
+        const res = await this.qrcodeToken({ uuid: this.uuid })
+        console.log(res)
+        if (res.errcode === 0) {
+          this.$message.success('登录成功')
+          sessionStorage.setItem('token', res.data.token)
+          sessionStorage.setItem('userName', res.data.userinfo.userName)
+          this.$router.push('/frame')
+        }
+      }
+    },
+    async submitForm (formName) {
+      this.$refs[formName].validate(async valid => {
+        if (valid) {
+          const res = await this.login(this.loginForm)
+          if (res && res.errcode === 0) {
+            sessionStorage.setItem('token', res.token)
+            sessionStorage.setItem('userName', res.userinfo.userName)
+            this.$router.push('/frame')
+            return
+          }
+          this.codeLoad()
+        } else {
+          this.codeLoad()
+          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
+      })
+    },
+    // 随机验证码
+    async codeLoad () {
+      const uuid = UUID.generate()
+      this.loginForm.state = uuid
+      this.codeUrl = `/api/gaf/verify/${uuid}.jpg`
+    }
+  },
+  mounted () {
+    this.codeLoad()
+  }
+}
+</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;
+}
+.tabs {
+  color: #fff;
+  div {
+    cursor: pointer;
+  }
+}
+.qrshow {
+  margin: 5% auto;
+}
+.code {
+  display: block;
+  height: 2.5em;
+}
+</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>

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

@@ -0,0 +1,143 @@
+<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 userMenuList from '../../store/gaf/menu'
+
+import { mapActions } from 'vuex'
+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,
+      userMenuList
+    }
+  },
+  methods: {
+    ...mapActions(['init'])
+  },
+  async mounted () {
+    await this.init()
+    // 登录超时检查
+    const worker = new Worker('./worker/token.js')
+    const token = sessionStorage.getItem('token')
+    worker.postMessage({ token })
+    const _this = this
+    let num = 0
+    worker.onmessage = function (e) {
+      if (e.data.errcode !== 0 && _this.$route.path !== '/') {
+        num++
+        if (num === 1) {
+          _this.$alert('请重新登录', '登录过期', {
+            confirmButtonText: '确定',
+            callback: () => {
+              _this.$router.push('/')
+              num = 0
+            }
+          })
+        }
+        worker.terminate()
+      }
+    }
+  },
+  computed: {
+    // 菜单计算属性
+    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>

+ 205 - 0
src/views/gaf/code.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="containerbox">
+    <el-card class="box-card nafdeep">
+      <div slot="header" class="clearfix">
+        <span>字典分类</span>
+        <el-button style="float: right; padding: 3px 0" type="text" @click="addtype">添加分类</el-button>
+      </div>
+      <deep-tree :data="treedata" @treeclick="treeclick" :operation="true" @delete="treedelete" @edit="treeedit"></deep-tree>
+    </el-card>
+    <el-card class="box-card nafgrid">
+      <div slot="header" class="clearfix">
+        <span>字典管理</span>
+        <el-button style="float: right; padding: 3px 0" type="text" @click="addcode">添加字典</el-button>
+      </div>
+      <div class="main">
+        <naf-grid class="grid" ref="grid" :data="codeList" :meta="meta" @delete="codedel" @edit="edit" :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 deepTree from '@naf/data/deep-tree'
+import dialogDrawer from '@naf/data/dialog -drawer'
+import nafForm from '@naf/data/form'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('code')
+export default {
+  components: {
+    nafGrid,
+    deepTree,
+    dialogDrawer,
+    nafForm
+  },
+  data () {
+    return {
+      typedata: null,
+      istype: false,
+      is_data: {},
+      visible: false,
+      formmeta: [
+        { name: 'name', title: '名称', filter: true },
+        { name: 'code', title: '编码', filter: true }
+      ],
+      meta: [
+        { name: 'name', title: '名称', filter: true },
+        { name: 'code', title: '编码', filter: true }
+      ],
+      rules: {
+        name: [
+          { required: true, message: '请输入名称', trigger: 'blur' }
+        ],
+        code: [
+          { required: true, message: '请输入编码', trigger: 'blur' }
+        ],
+        key: [
+          { required: true, message: '请输入key', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['typecreate', 'querytype', 'typeupdate', 'typedelete', 'codequery', 'codecreate', 'codeupdate', 'codedelete']),
+    // 查询分类
+    async typelist ({ filter = {}, paging = {} } = {}) {
+      await this.querytype()
+    },
+    // 删除分类
+    async treedelete ({ code }) {
+      const res = await this.typedelete({ code })
+      if (res.errcode === 0) {
+        this.$message.success('操作成功')
+        this.visible = false
+        await this.typelist()
+      }
+    },
+    // 修改分类
+    treeedit (e) {
+      this.istype = true
+      this.visible = true
+      this.is_data = e
+      this.formmeta.push({ name: 'key', title: 'key', filter: true })
+    },
+    // 查询字典
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.codequery({ filter, paging, category: this.typedata.code })
+    },
+    // 修改字典
+    edit (e) {
+      this.is_data = e
+      this.istype = false
+      if (this.formmeta.length > 2) {
+        this.formmeta.splice(2, 1)
+      }
+      this.visible = true
+    },
+    // 删除字典
+    async codedel (e) {
+      e.category = this.typedata.code
+      const res = await this.codedelete(e)
+      if (res.errcode === 0) {
+        this.$message.success('操作成功')
+        this.visible = false
+        await this.query()
+      }
+    },
+    // 点击分类
+    treeclick (e) {
+      this.typedata = e
+      this.query()
+    },
+    // 添加字典分类
+    addtype () {
+      this.istype = true
+      this.visible = true
+      this.is_data = {}
+      this.formmeta.push({ name: 'key', title: 'key', filter: true })
+    },
+    // 添加字典
+    addcode () {
+      this.is_data = {}
+      this.istype = false
+      if (this.formmeta.length > 2) {
+        this.formmeta.splice(2, 1)
+      }
+      this.visible = true
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+      this.is_data = {}
+    },
+    // 保存数据
+    async save (e) {
+      if (this.istype) {
+        let res
+        // 添加分类
+        if (!this.isNew) {
+          // 添加
+          res = await this.typecreate(e)
+        } else {
+          // 修改
+          res = await this.typeupdate(e)
+        }
+        if (res.errcode === 0) {
+          this.$message.success('操作成功')
+          this.visible = false
+          await this.typelist()
+        }
+      } else {
+        let res
+        e.category = this.typedata.code
+        // 添加字典
+        if (!this.isNew) {
+          // 添加
+          res = await this.codecreate(e)
+        } else {
+          // 修改
+          res = await this.codeupdate(e)
+        }
+        if (res.errcode === 0) {
+          this.$message.success('操作成功')
+          this.visible = false
+          await this.query()
+        }
+      }
+    }
+  },
+  async mounted () {
+    this.typelist()
+  },
+  computed: {
+    ...mapState(['codeList', 'total', 'treedata']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.containerbox {
+  height: 100%;
+  display: flex;
+  .nafdeep {
+    width: 15%;
+  }
+  .nafgrid {
+    width: 85%;
+  }
+}
+.box-card {
+  height: 100%;
+  /deep/ .el-card__body {
+    height: 100%;
+    width: 100%;
+  }
+}
+</style>

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

@@ -0,0 +1,57 @@
+<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 },
+        { name: 'date', title: '时间' },
+        { name: 'userName', title: '操作人' },
+        { name: 'ip', title: 'ip地址' }
+      ]
+    }
+  },
+  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>

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

@@ -0,0 +1,185 @@
+<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" @qr="qr" :operation="operation"></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>
+    <dialog-drawer type="dialog" :visible="qrshow" title="微信绑定" @close="qrshow = false" :width="'30%'">
+      <template v-slot:content>
+        <vue-qr class="qrshow" :text="downloadData.url" :margin="0" colorDark="#f67b29" colorLight="#fff" :logoSrc="downloadData.icon" :logoScale="0.3" :size="200"></vue-qr>
+        <div class="qrtext">请扫描二维码</div>
+      </template>
+    </dialog-drawer>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue'
+import nafGrid from '@naf/data/tables/naf-grid'
+import dialogDrawer from '@naf/data/dialog -drawer'
+import nafForm from '@naf/data/form'
+import vueQr from 'vue-qr'
+import { createNamespacedHelpers } from 'vuex'
+const { mapState, mapActions } = createNamespacedHelpers('adminuser')
+export default {
+  components: {
+    nafGrid,
+    dialogDrawer,
+    nafForm,
+    vueQr
+  },
+  data () {
+    return {
+      downloadData: {},
+      qrshow: false,
+      is_data: {},
+      visible: false,
+      operation: [
+        { name: 'qr', title: '绑定', icons: 'el-icon-mobile-phone' },
+        { name: 'edit', title: '编辑', icons: 'el-icon-edit' },
+        { name: 'delete', title: '删除', icons: 'el-icon-delete' }
+      ],
+      meta: [
+        { name: 'userName', title: '用户名', filter: true },
+        { name: 'status', title: '状态', formatter: 'status' }
+      ],
+      formmeta: [
+        { name: 'userName', title: '用户名' },
+        { name: 'password', title: '密码' },
+        { name: 'status', title: '状态', formatter: 'status' }
+      ],
+      rules: {
+        userName: [
+          { required: true, message: '请输入用户名', trigger: 'blur' }
+        ],
+        status: [
+          { required: true, message: '请输入状态', trigger: 'blur' }
+        ],
+        password: [
+          { required: true, message: '请输入密码', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['getUser', 'usercreate', 'userupdate', 'userdelete']),
+    // 绑定微信
+    qr (e) {
+      const userName = e.userName
+      this.downloadData = {
+        url: `${Vue.config.weixin.baseUrl}/api/weixin/bind?userName=${userName}`,
+        icon: ''
+      }
+      this.qrshow = true
+      this.$stomp({ '/exchange/qrcode.topic/bind': this.onMessage })
+    },
+    onMessage (msg) {
+      if (msg.body === 'success') {
+        this.qrshow = false
+        this.$message.success('操作成功')
+      }
+    },
+    // 添加
+    adduser () {
+      this.is_data = {}
+      this.visible = 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
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.getUser({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      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%;
+}
+.qrshow {
+  display: block !important;
+  width: 40%;
+  margin: 0 auto;
+}
+.qrtext {
+  width: 100%;
+  line-height: 3em;
+  font-size: 1.5em;
+  font-weight: 600;
+  text-align: center;
+}
+</style>

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

@@ -0,0 +1,308 @@
+<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="columns"
+          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="/tyylfiles/upload"
+              :show-file-list="false"
+              :on-success="handleAvatarSuccess"
+              :before-upload="beforeAvatarUpload"
+              :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>
+            <!-- 富文本 -->
+            <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('content')
+export default {
+  components: {
+    nafGrid,
+    nafForm,
+    editorBar
+  },
+  data () {
+    return {
+      columns: null,
+      is_title: null,
+      isClear: false,
+      detail: '',
+      myHeaders: { Authorization: `Bearer ${token}` },
+      imageUrl: '',
+      fileList: [],
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'title', title: '标题', filter: true },
+        { name: 'slug', title: '摘要' }
+      ],
+      formmeta: [
+        { name: 'thumbnail', title: '缩略图', slots: 'field' },
+        { name: 'title', title: '标题' },
+        { name: 'column', title: '绑定栏目', formatter: 'columns' },
+        { name: 'slug', title: '摘要' },
+        { name: 'content', title: '内容', slots: 'field' }
+      ],
+      rules: {
+        title: [
+          { required: true, message: '请输入标题', trigger: 'blur' }
+        ],
+        slug: [
+          { required: true, message: '请输入摘要', trigger: 'blur' }
+        ],
+        column: [
+          { required: true, message: '请选择绑定栏目', trigger: 'blur' }
+        ],
+        thumbnail: [
+          { required: true, message: '请上传缩略图', trigger: 'blur' }
+        ],
+        content: [
+          { required: true, message: '请输入内容', trigger: 'blur' }
+        ]
+      },
+      defaultProps: {
+        children: 'children',
+        label: 'name'
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['contentquery', 'contentcreate', 'contentupdate', 'contentdelete', 'contentdetails']),
+    // 点击树
+    treeClick (data) {
+      this.is_title = data.name
+      this.data = data
+      this.query()
+    },
+    // 添加
+    async addcontent () {
+      this.is_data = {
+        column: 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.contentItem.thumbnail) this.imageUrl = this.contentItem.thumbnail
+      this.visible = true
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      filter.column = 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 = ''
+    },
+    // 图片上传
+    // 文件上传成功时的钩子
+    handleAvatarSuccess (res, file) {
+      this.is_data = { ...this.$refs.ruleForm.form }
+      this.is_data.thumbnail = res.uri
+      this.$refs.ruleForm.formChage('thumbnail', res.uri)
+      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 () {
+    // 获取字典
+    this.columns = this.$dict('columns')
+    // eslint-disable-next-line eqeqeq
+    if (this.columns.length > 0) {
+      this.is_title = this.columns[0].name
+      this.$refs.deeptree.setCurrentKey(this.columns[0].code)
+      this.data = this.columns[0]
+      this.query()
+    }
+  },
+  computed: {
+    ...mapState(['total', 'contentList', 'contentItem']),
+    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>

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

@@ -0,0 +1,206 @@
+<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="pageList" :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 }">
+            <!-- 富文本 -->
+            <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')
+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: 'code', title: '编码', filter: true }
+      ],
+      formmeta: [
+        { name: 'title', title: '标题' },
+        { name: 'code', title: '编码' },
+        { name: 'content', title: '内容', slots: 'field' }
+      ],
+      rules: {
+        title: [
+          { required: true, message: '请输入标题', trigger: 'blur' }
+        ],
+        code: [
+          { required: true, message: '请输入编码', trigger: 'blur' }
+        ],
+        content: [
+          { required: true, message: '请输入内容', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['pagequery', 'pagecreate', 'pageupdate', 'pagedelete', 'pagedetails']),
+    // 添加
+    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({ code: e.code })
+      this.is_data = { ...this.pageItem }
+      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()
+  },
+  computed: {
+    ...mapState(['total', 'pageList', 'pageItem']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    }
+  }
+}
+</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>

+ 318 - 0
src/views/wokes/hospital/hospital.vue

@@ -0,0 +1,318 @@
+<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="addhospital">添加医院</el-button>
+      </div>
+      <div class="main">
+        <el-tree
+          :data="region"
+          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="deletehospital" :data="hospitalList" :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="/tyylfiles/upload"
+              :show-file-list="false"
+              :on-success="handleAvatarSuccess"
+              :before-upload="beforeAvatarUpload"
+              :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>
+            <!-- 富文本 -->
+            <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('hospital')
+export default {
+  components: {
+    nafGrid,
+    nafForm,
+    editorBar
+  },
+  data () {
+    return {
+      region: null,
+      is_title: null,
+      isClear: false,
+      detail: '',
+      myHeaders: { Authorization: `Bearer ${token}` },
+      imageUrl: '',
+      fileList: [],
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'name', title: '医院名称', filter: true },
+        { name: 'level', title: '医院级别' },
+        { name: 'code', title: '医院编码' }
+      ],
+      formmeta: [
+        { name: 'thumbnail', title: '缩略图', slots: 'field' },
+        { name: 'name', title: '医院名称' },
+        { name: 'code', title: '医院编码' },
+        { name: 'region', title: '绑定区域', formatter: 'region' },
+        { name: 'level', title: '医院级别' },
+        { name: 'address', title: '医院地址' },
+        { name: 'content', title: '内容', slots: 'field' }
+      ],
+      rules: {
+        name: [
+          { required: true, message: '请输入医院名称', trigger: 'blur' }
+        ],
+        code: [
+          { required: true, message: '请输入医院编码', trigger: 'blur' }
+        ],
+        region: [
+          { required: true, message: '请选择绑定区域', trigger: 'blur' }
+        ],
+        level: [
+          { required: true, message: '请输入医院等级', trigger: 'blur' }
+        ],
+        address: [
+          { required: true, message: '请输入医院地址', trigger: 'blur' }
+        ],
+        thumbnail: [
+          { required: true, message: '请上传缩略图', trigger: 'blur' }
+        ],
+        content: [
+          { required: true, message: '请输入内容', trigger: 'blur' }
+        ]
+      },
+      defaultProps: {
+        children: 'children',
+        label: 'name'
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['hospitalquery', 'hospitalcreate', 'hospitalupdate', 'hospitaldelete', 'hospitaldetails']),
+    // 点击树
+    treeClick (data) {
+      this.is_title = data.name
+      this.data = data
+      this.query()
+    },
+    // 添加
+    async addhospital () {
+      this.is_data = {
+        column: this.data.code
+      }
+      this.visible = true
+    },
+    // 删除
+    async deletehospital (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.hospitaldelete(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.hospitaldetails({ _id: e._id })
+      this.is_data = { ...this.hospitalItem }
+      if (this.hospitalItem.thumbnail) this.imageUrl = this.hospitalItem.thumbnail
+      this.visible = true
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      filter.region = this.data.code
+      await this.hospitalquery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      if (this.isNew) {
+        // 修改
+        res = await this.hospitalupdate(e)
+      } else {
+        // 添加
+        res = await this.hospitalcreate(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 = ''
+    },
+    // 图片上传
+    // 文件上传成功时的钩子
+    handleAvatarSuccess (res, file) {
+      this.is_data = { ...this.$refs.ruleForm.form }
+      this.is_data.thumbnail = res.uri
+      this.$refs.ruleForm.formChage('thumbnail', res.uri)
+      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 () {
+    // 获取字典
+    this.region = this.$dict('region')
+    // eslint-disable-next-line eqeqeq
+    if (this.region.length > 0) {
+      this.is_title = this.region[0].name
+      this.$refs.deeptree.setCurrentKey(this.region[0].code)
+      this.data = this.region[0]
+      this.query()
+    }
+  },
+  computed: {
+    ...mapState(['total', 'hospitalList', 'hospitalItem']),
+    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>

+ 353 - 0
src/views/wokes/hospital/specialist.vue

@@ -0,0 +1,353 @@
+<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="addspecialist">添加专家</el-button>
+      </div>
+      <div class="main">
+        <div class="left">
+          <el-select class="select" size="mini" v-model="options" @change="optionschange" placeholder="请选择">
+            <el-option
+              v-for="item in hospitalList"
+              :key="item.code"
+              :label="item.name"
+              :value="item.code">
+            </el-option>
+          </el-select>
+          <el-tree
+            :data="subject"
+            default-expand-all
+            :props="defaultProps"
+            @node-click="treeClick"
+            node-key="code"
+            ref="deeptree"
+            class="deeptree"
+          >
+          </el-tree>
+        </div>
+        <el-card class="maincard">
+          <div slot="header" class="clearfix">
+            <span>所属科室:{{ is_title }}</span>
+          </div>
+            <naf-grid class="grid" ref="grid" @edit="edit" @delete="deletespecialist" :data="specialistList" :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="/tyylfiles/upload"
+              :show-file-list="false"
+              :on-success="handleAvatarSuccess"
+              :before-upload="beforeAvatarUpload"
+              :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>
+            <!-- 富文本 -->
+            <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, mapMutations } from 'vuex'
+const token = sessionStorage.getItem('token')
+const { mapState, mapActions } = createNamespacedHelpers('specialist')
+const { mapState: subjectmapState, mapActions: subjectmapActions } = createNamespacedHelpers('subject')
+const { mapState: hospitalmapState, mapActions: hospitalmapActions } = createNamespacedHelpers('hospital')
+export default {
+  components: {
+    nafGrid,
+    nafForm,
+    editorBar
+  },
+  data () {
+    return {
+      options: null,
+      subject: null,
+      is_title: null,
+      isClear: false,
+      detail: '',
+      myHeaders: { Authorization: `Bearer ${token}` },
+      imageUrl: '',
+      fileList: [],
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'name', title: '专家名称', filter: true },
+        { name: 'code', title: '专家编码' }
+      ],
+      formmeta: [
+        { name: 'thumbnail', title: '缩略图', slots: 'field' },
+        { name: 'name', title: '专家名称' },
+        { name: 'code', title: '专家编码' },
+        { name: 'ability', title: '擅长领域' },
+        { name: 'money', title: '价格(元)' },
+        { name: 'content', title: '内容', slots: 'field' }
+      ],
+      rules: {
+        money: [
+          { required: true, message: '请输入价格(元)', trigger: 'blur' }
+        ],
+        name: [
+          { required: true, message: '请输入专家名称', trigger: 'blur' }
+        ],
+        code: [
+          { required: true, message: '请输入专家编码', trigger: 'blur' }
+        ],
+        ability: [
+          { required: true, message: '请输入擅长领域室', trigger: 'blur' }
+        ],
+        thumbnail: [
+          { required: true, message: '请上传缩略图', trigger: 'blur' }
+        ],
+        content: [
+          { required: true, message: '请输入内容', trigger: 'blur' }
+        ]
+      },
+      defaultProps: {
+        children: 'children',
+        label: 'name'
+      }
+    }
+  },
+  methods: {
+    ...mapMutations(['setdice']),
+    ...mapActions(['specialistquery', 'specialistcreate', 'specialistupdate', 'specialistdelete', 'specialistdetails']),
+    ...subjectmapActions(['subjectquery']),
+    ...hospitalmapActions(['hospitalquery']),
+    // 选择改变
+    optionschange (e) {
+      this.getsubject({ code: this.options })
+    },
+    // 点击树
+    treeClick (data) {
+      this.is_title = data.name
+      this.data = data
+      this.query()
+    },
+    // 添加
+    async addspecialist () {
+      this.is_data = {
+        hospitalId: this.data.hospitalId,
+        subjectId: this.data.code,
+        subjectName: this.data.name
+      }
+      this.visible = true
+    },
+    // 删除
+    async deletespecialist (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.specialistdelete(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.specialistdetails({ _id: e._id })
+      this.is_data = { ...this.specialistItem }
+      if (this.specialistItem.thumbnail) this.imageUrl = this.specialistItem.thumbnail
+      this.visible = true
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      filter.subjectId = this.data.code
+      await this.specialistquery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      if (this.isNew) {
+        // 修改
+        res = await this.specialistupdate(e)
+      } else {
+        // 添加
+        res = await this.specialistcreate(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 = ''
+      }
+    },
+    // 获取科室
+    async getsubject ({ code }) {
+      const res = await this.subjectquery({ filter: { hospitalId: code }, paging: { page: 1, size: 500 } })
+      this.setdice({ type: 'subject', list: res.data })
+      // 获取字典
+      this.subject = this.$dict('subject')
+      // eslint-disable-next-line eqeqeq
+      if (this.subject.length > 0) {
+        this.is_title = this.subject[0].name
+        this.$refs.deeptree.setCurrentKey(this.subject[0].code)
+        this.data = this.subject[0]
+        this.query()
+      }
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+      this.fileList = []
+      this.imageUrl = ''
+    },
+    // 图片上传
+    // 文件上传成功时的钩子
+    handleAvatarSuccess (res, file) {
+      this.is_data = { ...this.$refs.ruleForm.form }
+      this.is_data.thumbnail = res.uri
+      this.$refs.ruleForm.formChage('thumbnail', res.uri)
+      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 () {
+    await this.hospitalquery({ filter: {}, paging: { page: 1, size: 500 } })
+    this.options = this.hospitalList[0].code
+    this.getsubject({ code: this.options })
+  },
+  computed: {
+    ...mapState(['total', 'specialistList', 'specialistItem']),
+    ...subjectmapState(['total', 'subjectList', 'subjectItem']),
+    ...hospitalmapState(['total', 'hospitalList', 'hospitalItem']),
+    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%;
+  .left{
+    width: 15%;
+    .select {
+      width: 90%;
+      margin: 5% auto;
+      margin-left: 5%;
+    }
+  }
+  .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>

+ 316 - 0
src/views/wokes/hospital/subject.vue

@@ -0,0 +1,316 @@
+<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="addsubject">添加科室</el-button>
+      </div>
+      <div class="main">
+        <el-tree
+          :data="hospital"
+          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="deletesubject" :data="subjectList" :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="/tyylfiles/upload"
+              :show-file-list="false"
+              :on-success="handleAvatarSuccess"
+              :before-upload="beforeAvatarUpload"
+              :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>
+            <!-- 富文本 -->
+            <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, mapMutations } from 'vuex'
+const token = sessionStorage.getItem('token')
+const { mapState, mapActions } = createNamespacedHelpers('subject')
+const { mapState: hospitalmapState, mapActions: hospitalmapActions } = createNamespacedHelpers('hospital')
+export default {
+  components: {
+    nafGrid,
+    nafForm,
+    editorBar
+  },
+  data () {
+    return {
+      hospital: null,
+      is_title: null,
+      isClear: false,
+      detail: '',
+      myHeaders: { Authorization: `Bearer ${token}` },
+      imageUrl: '',
+      fileList: [],
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'name', title: '科室名称', filter: true },
+        { name: 'code', title: '科室编码' }
+      ],
+      formmeta: [
+        { name: 'thumbnail', title: '缩略图', slots: 'field' },
+        { name: 'name', title: '科室名称' },
+        { name: 'code', title: '科室编码' },
+        // { name: 'hospitalId', title: '绑定医院', formatter: 'hospital' },
+        { name: 'content', title: '内容', slots: 'field' }
+      ],
+      rules: {
+        name: [
+          { required: true, message: '请输入科室名称', trigger: 'blur' }
+        ],
+        code: [
+          { required: true, message: '请输入科室编码', trigger: 'blur' }
+        ],
+        // hospitalId: [
+        //   { required: true, message: '请选择绑定医院', trigger: 'blur' }
+        // ],
+        thumbnail: [
+          { required: true, message: '请上传缩略图', trigger: 'blur' }
+        ],
+        content: [
+          { required: true, message: '请输入内容', trigger: 'blur' }
+        ]
+      },
+      defaultProps: {
+        children: 'children',
+        label: 'name'
+      }
+    }
+  },
+  methods: {
+    ...mapMutations(['setdice']),
+    ...mapActions(['subjectquery', 'subjectcreate', 'subjectupdate', 'subjectdelete', 'subjectdetails']),
+    ...hospitalmapActions(['hospitalquery']),
+    // 点击树
+    treeClick (data) {
+      this.is_title = data.name
+      this.data = data
+      this.query()
+    },
+    // 添加
+    async addsubject () {
+      this.is_data = {
+        hospitalId: this.data.code
+      }
+      this.visible = true
+    },
+    // 删除
+    async deletesubject (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.subjectdelete(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.subjectdetails({ _id: e._id })
+      this.is_data = { ...this.subjectItem }
+      if (this.subjectItem.thumbnail) this.imageUrl = this.subjectItem.thumbnail
+      this.visible = true
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      filter.hospitalId = this.data.code
+      await this.subjectquery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      if (this.isNew) {
+        // 修改
+        res = await this.subjectupdate(e)
+      } else {
+        // 添加
+        res = await this.subjectcreate(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 = ''
+    },
+    // 图片上传
+    // 文件上传成功时的钩子
+    handleAvatarSuccess (res, file) {
+      this.is_data = { ...this.$refs.ruleForm.form }
+      this.is_data.thumbnail = res.uri
+      this.$refs.ruleForm.formChage('thumbnail', res.uri)
+      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.hospitalquery({ filter: {}, paging: { page: 1, size: 500 } })
+    console.log(res)
+    this.setdice({ type: 'hospital', list: res })
+    // 获取字典
+    this.hospital = this.$dict('hospital')
+    // eslint-disable-next-line eqeqeq
+    if (this.hospital.length > 0) {
+      this.is_title = this.hospital[0].name
+      this.$refs.deeptree.setCurrentKey(this.hospital[0].code)
+      this.data = this.hospital[0]
+      this.query()
+    }
+  },
+  computed: {
+    ...mapState(['total', 'subjectList', 'subjectItem']),
+    ...hospitalmapState(['total', 'hospitalList', 'hospitalItem']),
+    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>

+ 213 - 0
src/views/wokes/order.vue

@@ -0,0 +1,213 @@
+<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="addorder">添加单页</el-button>
+      </div>
+      <div class="main">
+        <naf-grid ref="grid" @edit="edit" @delete="deleteorder" :data="orderList" :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"></naf-form>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import nafGrid from '@naf/data/tables/naf-grid'
+import nafForm from '@naf/data/form'
+import { createNamespacedHelpers } from 'vuex'
+const token = sessionStorage.getItem('token')
+const { mapState, mapActions } = createNamespacedHelpers('order')
+export default {
+  components: {
+    nafGrid,
+    nafForm
+  },
+  data () {
+    return {
+      isClear: false,
+      detail: '',
+      myHeaders: { Authorization: `Bearer ${token}` },
+      imageUrl: '',
+      fileList: [],
+      is_data: {},
+      visible: false,
+      meta: [
+        { name: 'name', title: '姓名', filter: true },
+        { name: 'phone', title: '手机号' },
+        { name: 'time', title: '订单时间' },
+        { name: 'money', title: '金额' },
+        { name: 'status', title: '付款状态', formatter: 'orderStatus', filter: true }
+      ],
+      formmeta: [
+        { name: 'name', title: '姓名' },
+        { name: 'phone', title: '手机号' },
+        { name: 'time', title: '订单时间' },
+        { name: 'money', title: '金额' },
+        { name: 'status', title: '付款状态', formatter: 'orderStatus' },
+        { name: 'openid', title: 'openid' },
+        { name: 'code', title: '行政区编码' },
+        { name: 'hospitalId', title: '医院id' },
+        { name: 'hospitalName', title: '医院名称' },
+        { name: 'subjectId', title: '科室id' },
+        { name: 'subjectName', title: '科室名称' },
+        { name: 'specialistId', title: '专家id' },
+        { name: 'specialistName', title: '专家名称' },
+        { name: 'remark', title: '备注' }
+      ],
+      rules: {
+        name: [
+          { required: true, message: '请输入姓名', trigger: 'blur' }
+        ],
+        phone: [
+          { required: true, message: '请输入手机号', trigger: 'blur' }
+        ],
+        time: [
+          { required: true, message: '请输入订单时间', trigger: 'blur' }
+        ],
+        money: [
+          { required: true, message: '请输入金额', trigger: 'blur' }
+        ],
+        status: [
+          { required: true, message: '请输入付款状态', trigger: 'blur' }
+        ],
+        openid: [
+          { required: true, message: '请输入openid', trigger: 'blur' }
+        ],
+        code: [
+          { required: true, message: '请输入行政区编码', trigger: 'blur' }
+        ],
+        hospitalId: [
+          { required: true, message: '请输入医院id', trigger: 'blur' }
+        ],
+        hospitalName: [
+          { required: true, message: '请输入医院名称', trigger: 'blur' }
+        ],
+        subjectId: [
+          { required: true, message: '请输入科室id', trigger: 'blur' }
+        ],
+        subjectName: [
+          { required: true, message: '请输入科室名称', trigger: 'blur' }
+        ],
+        specialistId: [
+          { required: true, message: '请输入专家id', trigger: 'blur' }
+        ],
+        specialistName: [
+          { required: true, message: '请输入专家名称', trigger: 'blur' }
+        ],
+        remark: [
+          { required: false, message: '请输入备注', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    ...mapActions(['orderquery', 'ordercreate', 'orderupdate', 'orderdelete', 'orderdetails']),
+    // 添加
+    async addorder () {
+      this.is_data = {}
+      this.visible = true
+    },
+    // 删除
+    async deleteorder (e) {
+      this.$confirm('请确认删除', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const res = await this.orderdelete(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.orderdetails({ _id: e._id })
+      this.is_data = { ...this.orderItem }
+      this.visible = true
+    },
+    // 查询
+    async query ({ filter = {}, paging = {} } = {}) {
+      await this.orderquery({ filter, paging })
+    },
+    // 保存按钮
+    async save (e) {
+      let res
+      if (this.isNew) {
+        // 修改
+        res = await this.orderupdate(e)
+      } else {
+        // 添加
+        res = await this.ordercreate(e)
+      }
+      // eslint-disable-next-line eqeqeq
+      if (res.errcode == 0) {
+        this.$message.success('操作成功')
+        this.query()
+        this.$refs.grid.resetpage(-1)
+        this.visible = false
+      }
+    },
+    // 关闭弹窗
+    close () {
+      this.visible = false
+      this.fileList = []
+      this.imageUrl = ''
+    }
+  },
+  async mounted () {
+    this.query()
+  },
+  computed: {
+    ...mapState(['total', 'orderList', 'orderItem']),
+    isNew () {
+      return Boolean(this.is_data && this.is_data._id)
+    }
+  }
+}
+</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>

+ 43 - 0
vue.config.js

@@ -0,0 +1,43 @@
+const path = require('path')
+const baseUrl = '/yl-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: {
+      '/ws': {
+        target: 'http://192.168.0.45:15674'
+        // target: 'http://192.168.4.1:7001'
+      },
+      '/api/': {
+        // target: 'http://192.168.0.81:8001'
+        // target: 'http://192.168.4.1:7001'
+        target: 'http://127.0.0.1:18070'
+      },
+      '/api/naf/': {
+        target: 'http://localhost:8002'
+        // target: 'http://192.168.4.1:7001'
+      },
+      '/tyylfiles/': {
+        target: 'http://localhost:8009',
+        pathRewrite: { '^/tyylfiles/': '/tyyl/' }
+        // target: 'http://192.168.4.1:7001'
+      },
+      '/files/tyyl/': {
+        target: 'http://localhost:8006'
+      }
+    }
+  }
+}