后台管理系统可拖拽式组件的设计思路(补充,内附源码)
本文是对 后台管理系统可拖拽式组件的设计思路 文章的一个补充,近段时间,对代码做了一个简单的整理,已上传 GitHub。
服务器停了,所以没有了在线演示,可以直接 clone 或者 fork 代码下来直接运行,内置了一个页面的 demo。
配置页面效果:
渲染页面效果:
上一篇文章介绍了拖拽化组件的大体思路:
- 数据结构的组装
- 组件列表的选择
- 组件的拖拽处理
- 组件的配置信息配置
- 请求的处理
- 下拉选项数据的处理
- table 组件的设计
- 按钮与弹窗的处理
- 弹窗与表格数据的联动
- 自定义插槽
二次优化的点
后面优化的点主要是下面三点:
- 多功能、开放式的 text 文本组件
- 页面层级的数据处理
- 国际化配置的设计思路
text 文本组件
text 这个文本组件,在最开始的时候,是没有考虑到到底如何去做,所以在第一版的时候,没有去动它。那时候想到的场景就是行数据的查看,开始想到这个功能的时候,其实是与编辑一起的,然后在弹窗的属性上加了一个只读状态来实现查看功能。
比如这样:
后面又想到还有消息详情那种 "标题","内容" 格式的情况,所以我后面将他做成了一个开放式的组件,涉及到的功能:
- 静态数据
- 动态数据
- 自定义文本
静态数据
就是我们要给用户的一些提示语之类的,由开发者写死在页面上的,比如:
然后再给它一个自定义样式的功能,样式的写法是与正常的 css 写法一样:
这样我们就可以把这个功能完全开放给用户,自定义内容和样式。
动态数据
text 组件加了一个属性,这个属性 prop 对应是数据字段,接接口返回的数据库数据,可以设置自定义的格式化方法,对接口数据进行格式转换。
效果:
自定义文本
自定义文本的动能主要用于对接口数据的处理,比如对数据的加减乘除,及字段的拼接。
配置:
效果:
自定义函数内部只要是 js 支持的都可以写,上面的自定义函数的第二个参数,是用来处理下拉数据显示的,具体用法,可以去看下内置的 demo,table 的状态列有用到。
页面层级的数据处理
如果系统本身不做路由tab的缓存处理(vue的keep-alive),之前的数据设计思路是 OK 的。
之前组件内部的全局数据,我是用了一个全局 Map 对象来缓存的,如果每次进来都是重新请求渲染的,那么这个 Map 对象都会更新最新的。但是如果一旦使用了页面缓存的模式,那么后面新渲染的组件全局属性就会覆盖之前所有的全局属性,这样就几乎用不了了。
所以在后面的更新中,在 Map 对象中增加了一个页面级的属性,新进来的页面,所有的全局属性都会放到 pageId 下面。
import globalMap from './global-map'
const globalParams = {
global: {
pageId: props.pageId,
pageCode: props.pageCode,
langCode: ''
},
searchData: {},
options: {},
dialogMap: new Map
}
globalMap.set(props.pageId, globalParams)
我们在做变量替换的时候,就避免了不同页面的数据隔离。
export const getApiInfo = (
url = '',
params = '',
vals: Record<string, any> = {},
pageId: string
) => {
// 获取当前页面的全局数据
const globalParams = globalMap.get(pageId)
const p = {
...vals,
...globalParams.global,
...globalParams.searchData
}
const newUrl = url.replace(/\{(.*?)\}/g, (a: string, b: string) => {
return Object.prototype.hasOwnProperty.call(p, b) ? p[b] : a
})
const newParams = params.replace(/\{(.*?)\}/g, (a: string, b: string) => {
return Object.prototype.hasOwnProperty.call(p, b) ? p[b] : a
})
const obj: Record<string, string> = {}
newParams.replace(/([a-zA-Z0-9]+?)=(.*?)(&|$)/g, (a: string, b: string, c: string) => {
obj[b] = c
return a
})
return {
url: newUrl,
params: obj
}
}
国际化配置的设计思路
首先国际化的配置,vue 的项目一般用 vue-i18n 插件,一般前端在配置的国际化的时候,是将语言包写在本地的。由于页面是动态配置出来的,所以在项目配置的时候,也不知道需要配置哪些字段,并且这套设计思路是可以直接在线上配置页面,不需要重新打包与发布,所以我们得设计一个方案,支持以上的场景。
首先得动态添加语言包。
import { createI18n } from 'vue-i18n'
import EnMessage from './en'
import CnMessage from './zh-cn'
import HkMessage from './zh-hk'
const messages: Record<string, any> = {
'en': EnMessage,
'zh-cn': CnMessage,
'zh-tw': HkMessage
}
const map: Record<string, string> = {
'zh-CN': 'zh-cn',
'zh-HK': 'zh-tw',
'en-US': 'en',
}
const langtype = window.localStorage.getItem('headerLang')
const localeType = map[langtype || 'zh-CN'] || langtype
let i18n: any = null, hasLoadLang = false
export default function setupI18n() {
if (i18n) {
return i18n
}
i18n = createI18n({
locale: localeType as string,
fallbackLocale: map['en-US'],
messages
})
return i18n
}
export function loadLocaleMessages(gbl: any, globalI18n: Record<string, any>) {
if (hasLoadLang) {
return
}
hasLoadLang = true
// set locale and locale message
const preConfig = gbl.getLocaleMessage(localeType)
gbl.setLocaleMessage(localeType, { ...preConfig, ...globalI18n.getLocaleMessage(localeType) })
}
在 index.vue 页面执行加载
import { useI18n } from 'vue-i18n'
import setupI18n, { loadLocaleMessages } from '../../locale'
const i18n = setupI18n()
// 加载多语言配置
loadLocaleMessages(i18n.global, useI18n())
所以我们在需要翻译的地方加上一个 i18n 的 key,在后面在遍历所有组件提取 i18n 的 key。
配置页面:
// ...
// 组件属性
{
"type": "column",
"properties": {
"label": "评论数",
"i18n": "article.commentNum", // 国际化的key
"align": "left",
"type": "default",
"fixed": "none",
"customText": "function fn(row, parse) {\n\n}",
"prop": "commentNum"
},
"id": "1b8f11a0-ce85-4487-ba84-29eb0cc0cd9d"
}
// ...
还要优化的是把所有的 i18n 字段加完是一个很麻烦的事,所以可以在语言包上做个约定。
- 语言包数据的结构只分两层,一层是页面的,一层是字段级的
- 在页面的属性上,加一个所有 i18n 的前缀,后面再去解析一遍 pageJson,提取所有的国际化字段
配置页面处理
// 页面属性
{
"type": "common",
"properties": {
"title": "文章管理",
"code": "articleManage",
"langCode": "pageLangCode",
"writeVariable": []
}
}
// 子组件
[
{
"type": "column",
"properties": {
"label": "评论数",
"i18n": "commentNum", // 国际化的key
"align": "left",
"type": "default",
"fixed": "none",
"customText": "function fn(row, parse) {\n\n}",
"prop": "commentNum"
},
"id": "1b8f11a0-ce85-4487-ba84-29eb0cc0cd9d"
}
// ...
]
// 解析结果
{
pageLangCode: {
commentNum: '评论数'
// ...
}
}
渲染页面处理
// 静态字段处理
<el-button
type="primary"
:size="data.properties.size"
:round="data.properties.round"
:icon="(Icons as any)[data.properties.searchIcon || 'Search']"
@click="confirmSearch"
>{{ $tr('dp.search') }}</el-button>
// 动态字段处理
<el-form-item
:label="$tr(field.properties, pageId)"
:prop="field.properties.prop"
:required="field.properties.required"
:label-width="field.properties.labelWidth && (field.properties.labelWidth + 'px')"
>
</el-form-item>
这里的语言转换方法,用的 i18n 的配置是组件自用一套,不跟项目的 i18n 一起用是因为组件有可能放如组件库单独打包,这样的话就会与项目的配置分隔开来,如果与项目一起打包的,我们也需要包装一下,来处理动态加载的场景。
// 多语言转换处理
export const $tr = (
props: Record<string, any> | string, pageId = '', label = 'label', prop = 'i18n'
) => {
if (!t) {
t = setupI18n().global.t
}
if (typeof props === 'string') {
return t(props)
}
const langCode = pageId && globalMap.get(pageId).global.langCode
if (props[prop] && langCode) {
return t(`${langCode}.${props[prop]}`)
}
return props[label]
}
结尾
组件功能大概就这样,功能是根据我们项目需求一点一点加的,加上时间比较紧,代码可能比较乱。
有兴趣的小伙伴,如果有更好的方法,可以一起优化哈。
相关阅读
转载自:https://juejin.cn/post/7082551831074717726