一个UI表单的构成,避免不了下拉框,多选框等标签,在开发这些标签时,通常会请求后台接口获取字典值进行动态渲染。定制化开发虽然实现简单,但会产生大量重复工作,解决这类问题的思路有哪些?文章对若依字典管理插件实现思路进行了探究,以此来开阔思路。

探究过程如下:

一、界面设计

访问若依管理系统-系统管理-字典管理

界面截图如下:

若依(ruoyi)字典管理插件实现思路探究

 

功能提供了字典类型及字典键值的管理

二、数据库设计

SYS_DICT_TYPE

若依(ruoyi)字典管理插件实现思路探究

SYS_DICT_DATA

使用到SYS_DICT_TYPE,SYS_DICT_DATA两张表,定义了字典类型,及对应字典键值,两者是一对多的关系,通过dict_type关联。

三、开发用例

以用户性别为例,

功能配置:

字典名称:用户性别
字典类型:sys_user_sex

字典数据

若依(ruoyi)字典管理插件实现思路探究

页面开发:

export default {
    name: "User",
    dicts: ['sys_normal_disable', 'sys_user_sex'],
    components: {
      Treeselect
    },
    ......
}
          <el-col :span="12">
            <el-form-item label="用户性别">
              <el-select v-model="form.sex" placeholder="请选择性别">
                <el-option v-for="dict in dict.type.sys_user_sex" :key="dict.value" :label="dict.label"
                  :value="dict.value"></el-option>
              </el-select>
            </el-form-item>
          </el-col>

实现效果:

若依(ruoyi)字典管理插件实现思路探究

 可以发现,通过灵活的功能配置,vue页面只要引入对应的字典类型,就可以使用字典数据,完成标签的动态渲染,接下来对源码进行分析,探究其实现过程。

四、源码分析

在开发用例中,发现vue自定义属性:dicts: ['sys_user_sex'],申明了vue实例需要引入的字典类型,那问题来了,vue实例是怎么通过这个自定义属性获取对应字典信息呢?

为了回答这个问题,需要先了解一下main.js的使用,参考文章vue项目中main.js使用方法详解,

文中提到“main.js是我们的入口文件,主要作用是初始化vue实例,并引入所需要的插件”,根据提示在main.js中找到与字典插件相关的所有代码段:

import { getDicts } from "@/api/system/dict/data";
// 字典标签组件
import DictTag from '@/components/DictTag'
// 字典数据组件
import DictData from '@/components/DictData'
// 全局方法挂载
Vue.prototype.getDicts = getDicts
// 全局组件挂载
Vue.component('DictTag', DictTag)
DictData.install()

按照这些插件加载的先后顺序进行分析

import Vue from 'vue'
import DataDict from '@/utils/dict'
import { getDicts as getDicts } from '@/api/system/dict/data'
function install() {
  Vue.use(DataDict, {
    metas: {
      '*': {
        labelField: 'dictLabel',
        valueField: 'dictValue',
        request(dictMeta) {
          return getDicts(dictMeta.type).then(res => res.data)
        },
      },
    },
  })
}
export default {
  install,
}

其中 import DataDict from '@/utils/dict'  引入了字典数据组件,是整个实现的核心,是这次分析的重点,涉及以下插件:

若依(ruoyi)字典管理插件实现思路探究

其中index.js 是入口程序,实现逻辑如下:

import Dict from './Dict'
import { mergeOptions } from './DictOptions'
mergeOptions(options),将DictData的metas 与 DictOptions的metas配置信息进行合并
metas: {
  '*': {
    labelField: 'dictLabel',
    valueField: 'dictValue',
    request(dictMeta) {
      return getDicts(dictMeta.type).then(res => res.data)
    },
  },
}
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',
  },
},

什么是mixin?可查看博文对vue中mixin的理解。

其中data()方法 “const dict = new Dict()” 负责Dict组件创建,created() 方法中“this.dict.init(this.$options.dicts)”,将vue页面上定义的dicts数组传进去,组装数据,请求后端,获取对应字典数据。

import Dict from './Dict'
import { mergeOptions } from './DictOptions'
export default function(Vue, options) {
  mergeOptions(options)
  Vue.mixin({
    data() {
      if (this.$options === undefined || this.$options.dicts === undefined || this.$options.dicts === null) {
        return {}
      }
      const dict = new Dict()
      dict.owner = this
      return {
        dict
      }
    },
    created() {
      if (!(this.dict instanceof Dict)) {
        return
      }
      //  options 如果配置onCreated ,则执行options.onCreated
      options.onCreated && options.onCreated(this.dict)
      this.dict.init(this.$options.dicts).then(() => {
        //options 如果配置onReady ,则执行options.onReady方法
        options.onReady && options.onReady(this.dict)
        this.$nextTick(() => {
          this.$emit('dictReady', this.dict)
          if (this.$options.methods && this.$options.methods.onDictReady instanceof Function) {
            this.$options.methods.onDictReady.call(this, this.dict)
          }
        })
      })
    },
  })
}

对this.dict.init(this.$options.dicts)代码进行分析:其根据传入的options配置信息,生成字典元数据配置信息dictMeta,然后创建回调方法执行数组const ps = [],根据dictMeta加入对应字典类型的loadDict方法,通过Promise.all(ps)发送多个请求并根据请求顺序获取和使用字典数据。

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) {
    if (options instanceof Array) {
      options = { types: options }
    }
    const opts = mergeRecursive(DEFAULT_DICT_OPTIONS, options)
    if (opts.types === undefined) {
      throw new Error('need dict types')
    }
    const ps = []
    this._dictMetas = opts.types.map(t => DictMeta.parse(t))
    this._dictMetas.forEach(dictMeta => {
      const type = dictMeta.type
      Vue.set(this.label, type, {})
      Vue.set(this.type, type, [])
      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) {
  return dictMeta.request(dictMeta)
    .then(response => {
      const type = dictMeta.type
      let dicts = dictMeta.responseConverter(response, dictMeta)
      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].splice(0, Number.MAX_SAFE_INTEGER, ...dicts)
      dicts.forEach(d => {
        Vue.set(dict.label[type], d.value, d.label)
      })
      return dicts
    })
}

这样字典管理插件实现思路就基本清楚了,希望对你有所帮助。

发表回复