柏竹 柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档

柏竹

奋斗柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档
  • Java基础

  • JavaWeb

  • 拓展技术

  • 框架技术

  • 数据库

  • 数据结构

  • Spring

  • SpringMVC

  • SpringBoot

  • SpringClound

  • Ruoyi-Vue-Plus

    • ruoyi-vue-plus-基础功能
    • ruoyi-vue-plus-权限控制
    • ruoyi-vue-plus-表格操作
    • ruoyi-vue-plus-缓存功能
    • ruoyi-vue-plus-日志功能
    • ruoyi-vue-plus-线程相关
    • ruoyi-vue-plus-OSS功能
    • ruoyi-vue-plus-代码生成功能
    • ruoyi-vue-plus-多数据源
    • ruoyi-vue-plus-任务调度
    • ruoyi-vue-plus-监控功能
    • ruoyi-vue-plus-国际化
    • ruoyi-vue-plus-XSS功能
    • ruoyi-vue-plus-防重幂&限流 功能
    • ruoyi-vue-plus-推送功能
    • ruoyi-vue-plus-序列化功能
    • ruoyi-vue-plus-数据加密
    • ruoyi-vue-plus-单元测试
    • ruoyi-vue-plus-前端插件
      • 插件
        • 应用
        • Vue2用法
        • Vue3用法
      • 自定义指令钩子
        • 指令钩子引入
        • 权限&身份控制渲染
        • 权限校验 hasPermi
        • 身份校验 hasRole
        • 钩子应用示例
        • 复制剪贴 clipboard
        • 窗口拖拽 drag
        • 窗口宽度调整 dragWidth
        • 窗口高度跳转 dragHeight
      • 全局方法
        • 全局方法引入
        • 页签 tab
        • 认证 auth
        • 缓存 cache
        • 模态框 modal
        • 下载 download
      • 组件选项
        • 字典管理
        • 原理
        • 应用
    • ruoyi-vue-plus-前端工具篇
    • ruoyi-vue-plus-部署篇
    • ruoyi-vue-plus-前端篇
    • ruoyi-vue-plus-后端工具篇
    • ruoyi-vue-plus-框架篇
    • ruoyi-vue-plus-问题解决
  • 后端
  • Ruoyi-Vue-Plus
柏竹
2024-05-14
目录

ruoyi-vue-plus-前端插件

# 插件

插件通常为Vue设置全局功能 , 能添加全局 方法、资源、组件等功能 , 使用 Vue.prototype 上实现

Vue2文档 : https://v2.cn.vuejs.org (opens new window)

Vue3文档 : https://cn.vuejs.org (opens new window)

# 应用

创建需要暴露 install方法 , 安装函数本身 , 对其进行全局挂载

  • 全局方法
  • 注入组件选项
  • 全局资源

# Vue2用法

挂载插件

点击展开

main.js

import MyPlugin from './plugins/myPlugin'
// 后边参数可选
Vue.use(MyPlugin, { someOption: true })

src/plugins/myPlugin.js

// 插件js
const install = function (Vue, options) {
  // 1. 添加全局方法或 property
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3. 注入组件选项
  Vue.mixin({
    data() {},
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}
export default install

# Vue3用法

App实例应用 : https://cn.vuejs.org/ (opens new window) (自行查阅)

挂载插件

点击展开

main.js

import { createApp } from 'vue'
import MyPlugin from './plugins/myPlugin'
const app = createApp({})
app.use(MyPlugin, {
  /* 可选的选项 */
})

src/plugins/myPlugin.js

export default {
  install: (app, options) => {
    // 自定义 , 以下为示例
    // 注入一个全局可用的 $translate() 方法
    app.config.globalProperties.$translate = (key) => {
      // 获取 `options` 对象的深层属性
      // 使用 `key` 作为索引
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
  }
}

# 自定义指令钩子

官网文档 : https://cn.vuejs.org (opens new window) (概念理解)

# 指令钩子引入

点击展开

main.js

import directive from './directive'
Vue.use(directive)

src/directive/index.js

import hasRole from './permission/hasRole'
import hasPermi from './permission/hasPermi'
import dialogDrag from './dialog/drag'
import dialogDragWidth from './dialog/dragWidth'
import dialogDragHeight from './dialog/dragHeight'
import clipboard from './module/clipboard'

const install = function(Vue) {
  Vue.directive('hasRole', hasRole)
  Vue.directive('hasPermi', hasPermi)
  Vue.directive('clipboard', clipboard)
  Vue.directive('dialogDrag', dialogDrag)
  Vue.directive('dialogDragWidth', dialogDragWidth)
  Vue.directive('dialogDragHeight', dialogDragHeight)
}

// 兼容模式
if (window.Vue) {
  window['hasRole'] = hasRole
  window['hasPermi'] = hasPermi
  Vue.use(install); // eslint-disable-line
}
export default install

# 权限&身份控制渲染

根据 Vue 指令钩子控制元素渲染 , 从而实现限制显示 , 就是满足 权限/身份 的情况显示 组件/标签 , 未满足一律不显示 指令钩子 : v-hasRole、v-hasPermi

# 权限校验 hasPermi

权限标识钩子 src/directive/permission/hasPermi.js

点击展开
import store from '@/store'
export default {
  inserted(el, binding, vnode) {
    // 获取 自定义指令钩子 其值 (数组封装)
    const { value } = binding
    const all_permission = "*:*:*";
    // 权限列表
    const permissions = store.getters && store.getters.permissions

    if (value && value instanceof Array && value.length > 0) {
      const permissionFlag = value
	  
      // 判断是否包含
      const hasPermissions = permissions.some(permission => {
        return all_permission === permission || permissionFlag.includes(permission)
      })
	  
      // 不包含则删除当前节点的标签
      if (!hasPermissions) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`请设置操作权限标签值`)
    }
  }
}

# 身份校验 hasRole

身份标识钩子 src/directive/permission/hasRole.js

点击展开
import store from '@/store'
export default {
  inserted(el, binding, vnode) {
    const { value } = binding
    const super_admin = "admin";
    const roles = store.getters && store.getters.roles

    if (value && value instanceof Array && value.length > 0) {
      const roleFlag = value

      const hasRole = roles.some(role => {
        return super_admin === role || roleFlag.includes(role)
      })

      if (!hasRole) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`请设置角色权限标签值"`)
    }
  }
}

# 钩子应用示例

全局搜索 v-hasPermi 也是可以搜索到手的

随意截取部分也可以看到代码的约束和使用情况

点击展开
<el-col :span="1.5">
  <el-button
    type="primary"
    plain
    icon="el-icon-plus"
    size="mini"
    @click="handleAdd"
    v-hasPermi="['demo:demo:add']"
  >新增</el-button>
</el-col>
<el-col :span="1.5">
  <el-button
    type="success"
    plain
    icon="el-icon-edit"
    size="mini"
    :disabled="single"
    @click="handleUpdate"
    v-hasPermi="['demo:demo:edit']"
  >修改</el-button>
</el-col>
<el-col :span="1.5">
  <el-button
    type="danger"
    plain
    icon="el-icon-delete"
    size="mini"
    :disabled="multiple"
    @click="handleDelete"
    v-hasPermi="['demo:demo:remove']"
  >删除</el-button>
</el-col>

# 复制剪贴 clipboard

根据 Vue 指令钩子控制

指令钩子 : v-clipboard

依赖 : clipboarc 2.0.8 (剪切板依赖)

钩子arg标识

  • success 成功函数回调
  • error 失败函数回到
  • cut 剪切值
  • copy 复制值

提示

钩子arg标识 , 可为钩子提供类型操作的标识 , 已传递参数充当 . 例如在 v-my-directive:foo中,参数是"foo"

应用方式 : 写在触发事件的 按钮、a标签上 , 传递复制的值即可

复制粘贴钩子 src/directive/module/clipboard.js

点击展开
import Clipboard from 'clipboard'
export default {
  // 事件触发
  bind(el, binding, vnode) {
    switch (binding.arg) {
      case 'success':
        el._vClipBoard_success = binding.value;
        break;
      case 'error':
        el._vClipBoard_error = binding.value;
        break;
      default: {
        const clipboard = new Clipboard(el, {
          text: () => binding.value,
          action: () => binding.arg === 'cut' ? 'cut' : 'copy'
        });
        clipboard.on('success', e => {
          const callback = el._vClipBoard_success;
          callback && callback(e);
        });
        clipboard.on('error', e => {
          const callback = el._vClipBoard_error;
          callback && callback(e);
        });
        el._vClipBoard = clipboard;
      }
    }
  },
  // 变化剪切板, 更变元素触发
  update(el, binding) {
    if (binding.arg === 'success') {
      el._vClipBoard_success = binding.value;
    } else if (binding.arg === 'error') {
      el._vClipBoard_error = binding.value;
    } else {
      el._vClipBoard.text = function () { return binding.value; };
      el._vClipBoard.action = () => binding.arg === 'cut' ? 'cut' : 'copy';
    }
  },
  // 清除剪切板缓存, 关闭当前标签页触发
  unbind(el, binding) {
    if (!el._vClipboard) return
    if (binding.arg === 'success') {
      delete el._vClipBoard_success;
    } else if (binding.arg === 'error') {
      delete el._vClipBoard_error;
    } else {
      el._vClipBoard.destroy();
      delete el._vClipBoard;
    }
  }
}

# 窗口拖拽 drag

根据 Vue 指令钩子控制弹窗拖拽

指令钩子 : v-dialogDrag

应用方式 : 指令写在 el-dialog标签上即可 (elementUI)

弹窗拖拽钩子 src/directive/dialog/drag.js

点击展开
export default {
  bind(el, binding, vnode, oldVnode) {
    const value = binding.value
    if (value == false) return
    // 获取拖拽内容头部
    const dialogHeaderEl = el.querySelector('.el-dialog__header');
    const dragDom = el.querySelector('.el-dialog');
    dialogHeaderEl.style.cursor = 'move';
    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
    const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null);
    dragDom.style.position = 'absolute';
    dragDom.style.marginTop = 0;
    let width = dragDom.style.width;
    if (width.includes('%')) {
      width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100);
    } else {
      width = +width.replace(/\px/g, '');
    }
    dragDom.style.left = `${(document.body.clientWidth - width) / 2}px`;
    // 鼠标按下事件
    dialogHeaderEl.onmousedown = (e) => {
      // 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
      const disX = e.clientX - dialogHeaderEl.offsetLeft;
      const disY = e.clientY - dialogHeaderEl.offsetTop;

      // 获取到的值带px 正则匹配替换
      let styL, styT;

      // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
      if (sty.left.includes('%')) {
        styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100);
        styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100);
      } else {
        styL = +sty.left.replace(/\px/g, '');
        styT = +sty.top.replace(/\px/g, '');
      };

      // 鼠标拖拽事件
      document.onmousemove = function (e) {
        // 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
        const l = e.clientX - disX;
        const t = e.clientY - disY;

        let finallyL = l + styL
        let finallyT = t + styT

        // 移动当前元素
        dragDom.style.left = `${finallyL}px`;
        dragDom.style.top = `${finallyT}px`;

      };

      document.onmouseup = function (e) {
        document.onmousemove = null;
        document.onmouseup = null;
      };
    }
  }
};

# 窗口宽度调整 dragWidth

根据 Vue 指令钩子控制弹窗宽度拖拽调整

指令钩子 : v-dialogDragWidth

应用方式 : 指令写在 el-dialog标签上即可 (elementUI)

窗口宽度钩子 src/directive/dialog/dragWidth.js

点击展开
export default {
    bind(el) {
        const dragDom = el.querySelector('.el-dialog');
        const lineEl = document.createElement('div');
        lineEl.style = 'width: 6px; background: inherit; height: 10px; position: absolute; right: 0; bottom: 0; margin: auto; z-index: 1; cursor: nwse-resize;';
        lineEl.addEventListener('mousedown',
            function(e) {
                // 鼠标按下,计算当前元素距离可视区的距离
                const disX = e.clientX - el.offsetLeft;
                const disY = e.clientY - el.offsetTop;
                // 当前宽度 高度
                const curWidth = dragDom.offsetWidth;
                const curHeight = dragDom.offsetHeight;
                document.onmousemove = function(e) {
                    e.preventDefault(); // 移动时禁用默认事件
                    // 通过事件委托,计算移动的距离
                    const xl = e.clientX - disX;
                    const yl = e.clientY - disY
                    dragDom.style.width = `${curWidth + xl}px`;
                    dragDom.style.height = `${curHeight + yl}px`;
                };
                document.onmouseup = function(e) {
                    document.onmousemove = null;
                    document.onmouseup = null;
                };
            }, false);
        dragDom.appendChild(lineEl);
    }
}

# 窗口高度跳转 dragHeight

根据 Vue 指令钩子控制弹窗高度拖拽调整

指令钩子 : v-dialogDragHeight

应用方式 : 指令写在 el-dialog标签上即可 (elementUI)

窗口高度钩子 src/directive/dialog/dragHeight.js

点击展开
export default {
    bind(el) {
        const dragDom = el.querySelector('.el-dialog');
        const lineEl = document.createElement('div');
        lineEl.style = 'width: 5px; background: inherit; height: 80%; position: absolute; right: 0; top: 0; bottom: 0; margin: auto; z-index: 1; cursor: w-resize;';
        lineEl.addEventListener('mousedown',
            function (e) {
                // 鼠标按下,计算当前元素距离可视区的距离
                const disX = e.clientX - el.offsetLeft;
                // 当前宽度
                const curWidth = dragDom.offsetWidth;
                document.onmousemove = function (e) {
                    e.preventDefault(); // 移动时禁用默认事件
                    // 通过事件委托,计算移动的距离
                    const l = e.clientX - disX;
                    dragDom.style.width = `${curWidth + l}px`;
                };
                document.onmouseup = function (e) {
                    document.onmousemove = null;
                    document.onmouseup = null;
                };
            }, false);
        dragDom.appendChild(lineEl);
    }
}

# 全局方法

# 全局方法引入

点击展开

main.js

// plugins
import plugins from './plugins' 
Vue.use(plugins)

src/plugins/index.js

import tab from './tab'
import auth from './auth'
import cache from './cache'
import modal from './modal'
import download from './download'

export default {
  install(Vue) {
    // 页签操作
    Vue.prototype.$tab = tab
    // 认证对象
    Vue.prototype.$auth = auth
    // 缓存对象
    Vue.prototype.$cache = cache
    // 模态框对象
    Vue.prototype.$modal = modal
    // 下载文件
    Vue.prototype.$download = download
  }
}

// 外部引用方式
this.$tab.{方法名}

# 页签 tab

页签通用控制

方法

方法 参数为空情况下 说明
refreshPage(Object: <{ path, query }>) 刷新当前路由 刷新当前tab页签
closeOpenPage(Object: <{ path, query }>) 仅关闭当前页 关闭当前页签
closePage(Object: <{ path, query }>) 返回历史上页 , 没有上一页则为 "/" 关闭当前页签
closeAllPage() - 关闭所有tab页签
closeLeftPage(Object: <{ path, query }>) 引用当前路由 关闭左侧tab页签
closeRightPage(Object: <{ path, query }>) 引用当前路由 关闭右侧tab页签
closeOtherPage(Object: <{ path, query }>) 引用当前路由 关闭其他tab页签
openPage(title: String, url: String, params: Object) - 添加tab页签 , 并跳转目标页
updatePage(Object: <{ path, query }>) - 修改tab页签

src/plugins/tab.js

点击展开
import store from '@/store'
import router from '@/router';

export default {
  // 刷新当前tab页签
  refreshPage(obj) {
    const { path, query, matched } = router.currentRoute;
    if (obj === undefined) {
      matched.forEach((m) => {
        if (m.components && m.components.default && m.components.default.name) {
          if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
            obj = { name: m.components.default.name, path: path, query: query };
          }
        }
      });
    }
    return store.dispatch('tagsView/delCachedView', obj).then(() => {
      const { path, query } = obj
      router.replace({
        path: '/redirect' + path,
        query: query
      })
    })
  },
  // 关闭当前tab页签,打开新页签
  closeOpenPage(obj) {
    store.dispatch("tagsView/delView", router.currentRoute);
    if (obj !== undefined) {
      return router.push(obj);
    }
  },
  // 关闭指定tab页签
  closePage(obj) {
    if (obj === undefined) {
      return store.dispatch('tagsView/delView', router.currentRoute).then(({ visitedViews }) => {
        const latestView = visitedViews.slice(-1)[0]
        if (latestView) {
          return router.push(latestView.fullPath)
        }
        return router.push('/');
      });
    }
    return store.dispatch('tagsView/delView', obj);
  },
  // 关闭所有tab页签
  closeAllPage() {
    return store.dispatch('tagsView/delAllViews');
  },
  // 关闭左侧tab页签
  closeLeftPage(obj) {
    return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute);
  },
  // 关闭右侧tab页签
  closeRightPage(obj) {
    return store.dispatch('tagsView/delRightTags', obj || router.currentRoute);
  },
  // 关闭其他tab页签
  closeOtherPage(obj) {
    return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute);
  },
  // 添加tab页签
  openPage(title, url, params) {
    var obj = { path: url, meta: { title: title } }
    store.dispatch('tagsView/addView', obj);
    return router.push({ path: url, query: params });
  },
  // 修改tab页签
  updatePage(obj) {
    return store.dispatch('tagsView/updateVisitedView', obj);
  }
}

# 认证 auth

用户权限校验 , 根据 权限标识 & 角色维度校验

方法

方法 说明
hasPermi(permissions: String) 校验当前用户是否含有指定权限标识
hasPermiOr(permissions: Array<String>) 校验当前用户是否含有指定权限标识 (多个中的其中一个均可)
hasPermiAnd(permissions: Array<String>) 校验当前用户是否含有指定权限标识 (多个中的必须所有满足)
hasRole(roles: String) 校验当前用户是否为指定角色
hasRoleOr(roles: Array<String>) 校验当前用户是否为指定角色 (多个中的其中一个均可)
hasRoleAnd(roles: Array<String>) 校验当前用户是否为指定角色 (多个中的必须所有满足)

src/plugins/auth.js

点击展开
import store from '@/store'

function authPermission(permission) {
  const all_permission = "*:*:*";
  const permissions = store.getters && store.getters.permissions
  if (permission && permission.length > 0) {
    return permissions.some(v => {
      return all_permission === v || v === permission
    })
  } else {
    return false
  }
}

function authRole(role) {
  const super_admin = "admin";
  const roles = store.getters && store.getters.roles
  if (role && role.length > 0) {
    return roles.some(v => {
      return super_admin === v || v === role
    })
  } else {
    return false
  }
}

export default {
  // 验证用户是否具备某权限
  hasPermi(permission) {
    return authPermission(permission);
  },
  // 验证用户是否含有指定权限,只需包含其中一个
  hasPermiOr(permissions) {
    return permissions.some(item => {
      return authPermission(item)
    })
  },
  // 验证用户是否含有指定权限,必须全部拥有
  hasPermiAnd(permissions) {
    return permissions.every(item => {
      return authPermission(item)
    })
  },
  // 验证用户是否具备某角色
  hasRole(role) {
    return authRole(role);
  },
  // 验证用户是否含有指定角色,只需包含其中一个
  hasRoleOr(roles) {
    return roles.some(item => {
      return authRole(item)
    })
  },
  // 验证用户是否含有指定角色,必须全部拥有
  hasRoleAnd(roles) {
    return roles.every(item => {
      return authRole(item)
    })
  }
}

# 缓存 cache

Web缓存 , 排除 Vue状态管理缓存

Web存储方式

cookie (不考虑) localStorage (本机) sessionStorage (会话)
大小 4Kb 10Mb 5Mb
兼容 H4/H5 H5 H5
访问 任何窗口 任何窗口 同一窗口
有效期 手动设置 无 窗口关闭
存储位置 浏览器&服务器 浏览器 浏览器
与请求一同发送 Y N N
语法 复杂 简易 简易

方法

框架使用两种存储方式 本地local、会话Session 缓存

用法 :

  • this.$cache.local.set(key, value)
  • this.$cache.session.set(key, value)
方法 说明
{存储标识}.set(key, value) 存值
{存储标识}.get(key) 取值
{存储标识}.setJSON(key, jsonValue) 存json
{存储标识}.getJSON(key) 取json
{存储标识}.remove(key) 移除指定缓存

提示

该插件不支持存储对象object , 建议使用JSON化存储

src/plugins/cache.js

点击展开
const sessionCache = {
  set (key, value) {
    if (!sessionStorage) {
      return
    }
    if (key != null && value != null) {
      sessionStorage.setItem(key, value)
    }
  },
  get (key) {
    if (!sessionStorage) {
      return null
    }
    if (key == null) {
      return null
    }
    return sessionStorage.getItem(key)
  },
  setJSON (key, jsonValue) {
    if (jsonValue != null) {
      this.set(key, JSON.stringify(jsonValue))
    }
  },
  getJSON (key) {
    const value = this.get(key)
    if (value != null) {
      return JSON.parse(value)
    }
  },
  remove (key) {
    sessionStorage.removeItem(key);
  }
}
const localCache = {
  set (key, value) {
    if (!localStorage) {
      return
    }
    if (key != null && value != null) {
      localStorage.setItem(key, value)
    }
  },
  get (key) {
    if (!localStorage) {
      return null
    }
    if (key == null) {
      return null
    }
    return localStorage.getItem(key)
  },
  setJSON (key, jsonValue) {
    if (jsonValue != null) {
      this.set(key, JSON.stringify(jsonValue))
    }
  },
  getJSON (key) {
    const value = this.get(key)
    if (value != null) {
      return JSON.parse(value)
    }
  },
  remove (key) {
    localStorage.removeItem(key);
  }
}

export default {
  /**
   * 会话级缓存
   */
  session: sessionCache,
  /**
   * 本地缓存
   */
  local: localCache
}

# 模态框 modal

提示框 , 将Element提示框封装简化通用代码

方法 : 结合Element组件编程式实现 (方法省略)

src/plugins/modal.js

点击展开
import { Message, MessageBox, Notification, Loading } from 'element-ui'

let loadingInstance;

export default {
  // 消息提示
  msg(content) {
    Message.info(content)
  },
  // 错误消息
  msgError(content) {
    Message.error(content)
  },
  // 成功消息
  msgSuccess(content) {
    Message.success(content)
  },
  // 警告消息
  msgWarning(content) {
    Message.warning(content)
  },
  // 弹出提示
  alert(content) {
    MessageBox.alert(content, "系统提示")
  },
  // 错误提示
  alertError(content) {
    MessageBox.alert(content, "系统提示", { type: 'error' })
  },
  // 成功提示
  alertSuccess(content) {
    MessageBox.alert(content, "系统提示", { type: 'success' })
  },
  // 警告提示
  alertWarning(content) {
    MessageBox.alert(content, "系统提示", { type: 'warning' })
  },
  // 通知提示
  notify(content) {
    Notification.info(content)
  },
  // 错误通知
  notifyError(content) {
    Notification.error(content);
  },
  // 成功通知
  notifySuccess(content) {
    Notification.success(content)
  },
  // 警告通知
  notifyWarning(content) {
    Notification.warning(content)
  },
  // 确认窗体
  confirm(content) {
    return MessageBox.confirm(content, "系统提示", {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: "warning",
    })
  },
  // 提交内容
  prompt(content) {
    return MessageBox.prompt(content, "系统提示", {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: "warning",
    })
  },
  // 打开遮罩层
  loading(content) {
    loadingInstance = Loading.service({
      lock: true,
      text: content,
      spinner: "el-icon-loading",
      background: "rgba(0, 0, 0, 0.7)",
    })
  },
  // 关闭遮罩层
  closeLoading() {
    loadingInstance.close();
  }
}

# 下载 download

通用下载插件 , 采用二进制流处理

方法

方法 说明
oss(ossId: String) oss文件下载 , 按ossId
zip(url: String, name: String) 压缩包文件下载

src/plugins/download.js

点击展开
import axios from 'axios'
import {Loading, Message} from 'element-ui'
import { saveAs } from 'file-saver'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { blobValidate } from "@/utils/ruoyi";

const baseURL = process.env.VUE_APP_BASE_API
let downloadLoadingInstance;

export default {
  oss(ossId) {
    var url = baseURL + '/system/oss/download/' + ossId
    downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
    axios({
      method: 'get',
      url: url,
      responseType: 'blob',
      headers: { 'Authorization': 'Bearer ' + getToken() }
    }).then((res) => {
      const isBlob = blobValidate(res.data);
      if (isBlob) {
        const blob = new Blob([res.data], { type: 'application/octet-stream' })
        this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
      } else {
        this.printErrMsg(res.data);
      }
      downloadLoadingInstance.close();
    }).catch((r) => {
      console.error(r)
      Message.error('下载文件出现错误,请联系管理员!')
      downloadLoadingInstance.close();
    })
  },
  zip(url, name) {
    var url = baseURL + url
    downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
    axios({
      method: 'get',
      url: url,
      responseType: 'blob',
      headers: {
        'Authorization': 'Bearer ' + getToken(),
        'datasource': localStorage.getItem("dataName")
      }
    }).then((res) => {
      const isBlob = blobValidate(res.data);
      if (isBlob) {
        const blob = new Blob([res.data], { type: 'application/zip' })
        this.saveAs(blob, name)
      } else {
        this.printErrMsg(res.data);
      }
      downloadLoadingInstance.close();
    }).catch((r) => {
      console.error(r)
      Message.error('下载文件出现错误,请联系管理员!')
      downloadLoadingInstance.close();
    })
  },
  saveAs(text, name, opts) {
    saveAs(text, name, opts);
  },
  async printErrMsg(data) {
    const resText = await data.text();
    const rspObj = JSON.parse(resText);
    const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
    Message.error(errMsg);
  }
}

# 组件选项

# 字典管理

# 原理

大致过程

  1. 安装插件install
  2. mixin混入 data、created方法
  3. 执行 init() 初始化方法 , 传入 dict选项中的字典key
  4. 获取 字典key中的所有元数据配置
  5. 根据元数据进行获取字典信息并缓存
  6. 转换 适配字典字段名
  7. 异步 执行元数据加载集
  8. 回调 方法传递字典数据 和 触发methods事件中的onDictReady()方法

提示

元数据充当字典数据获取的主要对象

元数据解刨

export default class DictMeta {
  constructor(options) {
    // dict选项中的字典key
    this.type = options.type
    // 字典数据请求获取方法(含缓存)
    this.request = options.request
    // 请求数据字段名转换处理方法
    this.responseConverter = options.responseConverter
    // 转化字段名标识
    this.labelField = options.labelField
    this.valueField = options.valueField
    // 是否懒加载 (省略)
    this.lazy = options.lazy === true
  }
}

涉及文件 (执行顺序由上到下)

文件名 说明
components/DictData/index.js 组件选项安装
utils/dict/index.js 混入选项 , 初始化dict 和 注入字典
utils/dict/DictOptions.js 字典配置项
utils/dict/Dict.js 字典数据加载
utils/dict/DictMeta.js 元数据构建&解析
utils/dict/DictConverter.js 适配字段名
utils/dict/DictData.js 字典数据结构

源码 (附加注释更好理解)

main.js

点击展开
// 字典数据组件
import DictData from '@/components/DictData'
DictData.install()

components/DictData/index.js

点击展开
import Vue from 'vue'
import store from '@/store'
import DataDict from '@/utils/dict'
import { getDicts as getDicts } from '@/api/system/dict/data'

function searchDictByKey(dict, key) {
  if (key == null && key == "") {
    return null
  }
  try {
    for (let i = 0; i < dict.length; i++) {
      if (dict[i].key == key) {
        return dict[i].value
      }
    }
  } catch (e) {
    return null
  }
}

function install() {
  Vue.use(DataDict, {
    metas: {
      '*': {
        // 适配字段名
        labelField: 'dictLabel',
        valueField: 'dictValue',
        // 字典获取原头
        request(dictMeta) {
          const storeDict = searchDictByKey(store.getters.dict, dictMeta.type)
          if (storeDict) {
            return new Promise(resolve => { resolve(storeDict) })
          } else {
            return new Promise((resolve, reject) => {
              getDicts(dictMeta.type).then(res => {
                store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data })
                resolve(res.data)
              }).catch(error => {
                reject(error)
              })
            })
          }
        },
      },
    },
  })
}

export default {
  install,
}

utils/dict/Dict.js

点击展开
import Vue from 'vue'
import { mergeRecursive } from "@/utils/ruoyi";
import DictMeta from './DictMeta'
import DictData from './DictData'

const DEFAULT_DICT_OPTIONS = {
  types: [],
}

/**
 * @classdesc 字典
 * @property {Object} label 标签对象,内部属性名为字典类型名称
 * @property {Object} dict 字段数组,内部属性名为字典类型名称
 * @property {Array.<DictMeta>} _dictMetas 字典元数据数组
 */
export default class Dict {
  constructor() {
    this.owner = null
    this.label = {}
    this.type = {}
  }

  init(options) {
    // 包装 字典key
    console.log('5 => 接收dist中的应用类型字典的数组', options)
    if (options instanceof Array) {
      options = { types: options }
      console.log('6 => 调整结构, key为类型的对象', options)
    }
    const opts = mergeRecursive(DEFAULT_DICT_OPTIONS, options)
    console.log('7 => 合并配置项options(单独合并tpes)', opts, DEFAULT_DICT_OPTIONS)
    if (opts.types === undefined) {
      throw new Error('need dict types')
    }
    // 意图: 初始化字典元数据集 , 并遍历元数据加载字典信息
    const ps = []
    console.log('8 => 遍历types, 获取字典元数据', opts.types)
    this._dictMetas = opts.types.map(t => DictMeta.parse(t))
    console.log('10 => 获取types类型的所有数组元数据结果', this._dictMetas)
    // 遍历加载字典信息
    this._dictMetas.forEach(dictMeta => {
      const type = dictMeta.type
      // 初始化结构
      Vue.set(this.label, type, {})
      Vue.set(this.type, type, [])
      console.log('11 => dist数据', this)
      if (dictMeta.lazy) {
        return
      }
      ps.push(loadDict(this, dictMeta))
    })
    // 异步统一加载
    return Promise.all(ps)
  }

  /**
   * 重新加载字典
   * @param {String} type 字典类型
   */
  reloadDict(type) {
    const dictMeta = this._dictMetas.find(e => e.type === type)
    if (dictMeta === undefined) {
      return Promise.reject(`the dict meta of ${type} was not found`)
    }
    return loadDict(this, dictMeta)
  }
}

/**
 * 加载字典
 * @param {Dict} dict 字典
 * @param {DictMeta} dictMeta 字典元数据
 * @returns {Promise}
 */
function loadDict(dict, dictMeta) {
  // 调取元数据 request请求方法 按dictMeta.type获取 (该方法会先去缓存中获取)
  return dictMeta.request(dictMeta)
    .then(response => {
      console.log('12 => 通过vuex状态管理获取,如果没有发起请求获取', response)
      const type = dictMeta.type
      // 意图: 调整字段属性名 (转化方法在 DictOptions.js 选项配置中获取)
      let dicts = dictMeta.responseConverter(response, dictMeta)
      // 确保字典 数组类型 && 数组中包含有DictData对象
      if (!(dicts instanceof Array)) {
        console.error('the return of responseConverter must be Array.<DictData>')
        dicts = []
      } else if (dicts.filter(d => d instanceof DictData).length !== dicts.length) {
        console.error('the type of elements in dicts must be DictData')
        dicts = []
      }
      // 赋值支持以下两种使用方式
      // 支持 dict.type['type']使用方式
      dict.type[type].splice(0, Number.MAX_SAFE_INTEGER, ...dicts)
      // 支持 dist.label.type使用方式
      dicts.forEach(d => {
        Vue.set(dict.label[type], d.value, d.label)
      })
      console.log('13 => 将字典逐个写入', dicts)
      return dicts
    })
}

utils/dict/DictConverter.js

点击展开
import DictOptions from './DictOptions'
import DictData from './DictData'

export default function(dict, dictMeta) {
  // 从 dictOptions.DEFAULT_LABEL_FIELDS 和 元数据的字段 中查找匹配字段名的 label
  const label = determineDictField(dict, dictMeta.labelField, ...DictOptions.DEFAULT_LABEL_FIELDS)
  // 从 dictOptions.DEFAULT_VALUE_FIELDS 和 元数据的字段 中查找匹配字段名的 value
  const value = determineDictField(dict, dictMeta.valueField, ...DictOptions.DEFAULT_VALUE_FIELDS)
  // 构建当前dict
  return new DictData(dict[label], dict[value], dict)
}

/**
 * 确定字典字段
 * @param {DictData} dict
 * @param  {...String} fields
 */
function determineDictField(dict, ...fields) {
  // 遍历所有猜想相关的字段名fields , 获取dict字段 , 如果存在直接获取
  return fields.find(f => Object.prototype.hasOwnProperty.call(dict, f))
}

utils/dict/DictData.js

点击展开
/**
 * @classdesc 字典数据
 * @property {String} label 标签
 * @property {*} value 标签
 * @property {Object} raw 原始数据
 */
export default class DictData {
  constructor(label, value, raw) {
    this.label = label
    this.value = value
    this.raw = raw
  }
}

utils/dict/DictMeta.js

点击展开
import { mergeRecursive } from "@/utils/ruoyi";
import DictOptions from './DictOptions'

/**
 * @classdesc 字典元数据
 * @property {String} type 类型
 * @property {Function} request 请求
 * @property {String} label 标签字段
 * @property {String} value 值字段
 */
export default class DictMeta {
  constructor(options) {
    this.type = options.type
    this.request = options.request
    this.responseConverter = options.responseConverter
    this.labelField = options.labelField
    this.valueField = options.valueField
    this.lazy = options.lazy === true
  }
}


/**
 * 解析字典元数据
 * @param {Object} options
 * @returns {DictMeta}
 */
DictMeta.parse= function(options) {
  console.log('9 => 接收到类型', options, DictOptions.metas)
  let opts = null
  if (typeof options === 'string') {
    opts = DictOptions.metas[options] || {}
    opts.type = options
    // Object传递本身
  } else if (typeof options === 'object') {
    opts = options
  }
  // 合并 opts中的type
  opts = mergeRecursive(DictOptions.metas['*'], opts)
  return new DictMeta(opts)
}

utils/dict/DictOptions.js

点击展开
import { mergeRecursive } from "@/utils/ruoyi";
import dictConverter from './DictConverter'

export const options = {
  metas: {
    '*': {
      /**
       * 字典请求,方法签名为function(dictMeta: DictMeta): Promise
       */
      request: (dictMeta) => {
        console.log(`load dict ${dictMeta.type}`)
        return Promise.resolve([])
      },
      /**
       * 字典响应数据转换器,方法签名为function(response: Object, dictMeta: DictMeta): DictData
       */
      responseConverter,
      labelField: 'label',
      valueField: 'value',
    },
  },
  /**
   * 默认标签字段
   */
  DEFAULT_LABEL_FIELDS: ['label', 'name', 'title'],
  /**
   * 默认值字段
   */
  DEFAULT_VALUE_FIELDS: ['value', 'id', 'uid', 'key'],
}

/**
 * 映射字典
 * @param {Object} response 字典数据
 * @param {DictMeta} dictMeta 字典元数据
 * @returns {DictData}
 */
function responseConverter(response, dictMeta) {
  const dicts = response.content instanceof Array ? response.content : response
  if (dicts === undefined) {
    console.warn(`no dict data of "${dictMeta.type}" found in the response`)
    return []
  }
  // 遍历字典集
  return dicts.map(d => dictConverter(d, dictMeta))
}

export function mergeOptions(src) {
  mergeRecursive(options, src)
  console.log('2 => 合并配置项 src合并到options (覆盖合并)', options)
}

export default options

utils/dict/index.js

点击展开
import Dict from './Dict'
import { mergeOptions } from './DictOptions'

export default function(Vue, options) {
  console.log('1 => 安装插件拿到options配置项', options)
  // 意图: 将 options.metas['*'].request() 获取字典方法整合到默认配置中
  mergeOptions(options)
  console.log('3 => 合并配置项并未改变options (options默认配置项不影响)')
  Vue.mixin({
    // 混入 初始化的 dict字典集
    data() {
      if (this.$options === undefined || this.$options.dicts === undefined || this.$options.dicts === null) {
        return {}
      }
      const dict = new Dict()
      dict.owner = this
      console.log('4 => 实例初始化dict字典', dict)
      return {
        dict
      }
    },
    created() {
      // 确保上边data , dict初始化
      if (!(this.dict instanceof Dict)) {
        return
      }
      options.onCreated && options.onCreated(this.dict)
      this.dict.init(this.$options.dicts).then(() => {
        // 回调应用 (可忽视)
        options.onReady && options.onReady(this.dict)
        // 确保DOM更新后进行调取以下方法
        this.$nextTick(() => {
          // 通过方法传递dict对象
          this.$emit('dictReady', this.dict)
          // 该组件存在 methods 事件选项 , 并且存在 onDictReady()方法
          if (this.$options.methods && this.$options.methods.onDictReady instanceof Function) {
            // 回调 methods事件中的onDictReady()方法 , 并传递字典数据
            this.$options.methods.onDictReady.call(this, this.dict)
          }
        })
      })
    },
  })
}

# 应用

字典数据

  • dict.type['<字典key>']
  • dist.label.<字典key>

字典回调

  • 监听方案

    created() {
      this.$on("dictReady", () => { })
    }
    
  • 选项方法方案 (方法名固定)

    methods: {
      onDictReady(dict) { }
    }
    
#ruoyi-vue-plus#插件

← ruoyi-vue-plus-单元测试 ruoyi-vue-plus-前端工具篇→

最近更新
01
HTTPS自动续签
10-21
02
博客搭建-简化版(脚本)
10-20
03
ruoyi-vue-plus-部署篇
07-13
更多文章>
Theme by Vdoing | Copyright © 2019-2024 | 桂ICP备2022009417号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式