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);
}
}
# 组件选项
# 字典管理
# 原理
大致过程
- 安装插件install
- mixin混入 data、created方法
- 执行 init() 初始化方法 , 传入 dict选项中的字典key
- 获取 字典key中的所有元数据配置
- 根据元数据进行获取字典信息并缓存
- 转换 适配字典字段名
- 异步 执行元数据加载集
- 回调 方法传递字典数据 和 触发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 | 字典数据结构 |
源码 (附加注释更好理解)
点击展开
// 字典数据组件
import DictData from '@/components/DictData'
DictData.install()
点击展开
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,
}
点击展开
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
})
}
点击展开
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))
}
点击展开
/**
* @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
}
}
点击展开
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)
}
点击展开
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
点击展开
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) { } }