后台管理系统可拖拽式组件的设计思路(补充,内附源码)

本文是对 后台管理系统可拖拽式组件的设计思路 文章的一个补充,近段时间,对代码做了一个简单的整理,已上传 GitHub。

服务器停了,所以没有了在线演示,可以直接 clone 或者 fork 代码下来直接运行,内置了一个页面的 demo。

配置页面效果:

image.png

渲染页面效果:

image.png

上一篇文章介绍了拖拽化组件的大体思路:

  • 数据结构的组装
  • 组件列表的选择
  • 组件的拖拽处理
  • 组件的配置信息配置
  • 请求的处理
  • 下拉选项数据的处理
  • table 组件的设计
  • 按钮与弹窗的处理
  • 弹窗与表格数据的联动
  • 自定义插槽

二次优化的点

后面优化的点主要是下面三点:

  • 多功能、开放式的 text 文本组件
  • 页面层级的数据处理
  • 国际化配置的设计思路

text 文本组件

text 这个文本组件,在最开始的时候,是没有考虑到到底如何去做,所以在第一版的时候,没有去动它。那时候想到的场景就是行数据的查看,开始想到这个功能的时候,其实是与编辑一起的,然后在弹窗的属性上加了一个只读状态来实现查看功能。

比如这样:

image.png

后面又想到还有消息详情那种 "标题","内容" 格式的情况,所以我后面将他做成了一个开放式的组件,涉及到的功能:

  • 静态数据
  • 动态数据
  • 自定义文本

静态数据

就是我们要给用户的一些提示语之类的,由开发者写死在页面上的,比如:

image.png

然后再给它一个自定义样式的功能,样式的写法是与正常的 css 写法一样:

image.png

这样我们就可以把这个功能完全开放给用户,自定义内容和样式。

动态数据

text 组件加了一个属性,这个属性 prop 对应是数据字段,接接口返回的数据库数据,可以设置自定义的格式化方法,对接口数据进行格式转换。

image.png

效果:

image.png

自定义文本

自定义文本的动能主要用于对接口数据的处理,比如对数据的加减乘除,及字段的拼接。

配置:

image.png

效果:

image.png

自定义函数内部只要是 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]
}
复制代码

结尾

组件功能大概就这样,功能是根据我们项目需求一点一点加的,加上时间比较紧,代码可能比较乱。

有兴趣的小伙伴,如果有更好的方法,可以一起优化哈。

相关阅读

后台管理系统可拖拽式组件的设计思路

猜你喜欢

转载自juejin.im/post/7082551831074717726