Vue项目中的字段格式化工具(进阶版)

场景: 对于一些全局公用的状态,或是字典,甚至是后端枚举,为了方便维护,我们应尽量使它们全局公用,但我们在开发往往会忽略这个问题,总想着后面再改,可随着项目的不断推进,我们往往都视之不理。
功能: 解决vue项目中字段、字典、状态类全局维护问题。
优势: 一次配置全局公用、可单独变更、可自定义、低请求、方便、快捷。
特点: 组件化、全局化、公用化。

1. 创建组件

components 下创建 FieldFormat 包,然后在该包下创建 index.vue

2. 创建基础属性类

该类为所有属性类的基类,也用以规范属性列表。

FieldFormat 包下创建 Base.js,内容如下:

js 复制代码
export default class Base {
  constructor() {
    this.serve = undefined;
    this.id = undefined;
    this.label = undefined;
    this.method = 'get';
    this.dataField = 'data';
    this.isEnum = false;
    this.isDict = false;
    this.isCustom = false;
    this.customData = undefined;
    this.render = undefined;
    this.tagTypes = undefined;
  }

  /**
   * 添加tag属性,用以匹配el-tag样式
   * @param tags
   * @returns {Base}
   */
  tags(tags) {
    this.tagTypes = tags;
    return this;
  }

  /**
   * 添加自定义渲染,传入函数,将渲染返回的内容
   * @param render
   * @returns {Base}
   */
  renders(render) {
    this.render = render;
    return this;
  }
}

3. 创建自定义数据类

该类为自定义数据属性,可传入自定义的属性用于匹配字段。

FieldFormat 包下创建 Custom.js,内容如下:

js 复制代码
import Base from "./Base";

/**
 * 自定义数据
 */
export default class Custom extends Base {
  constructor(data, id, label) {
    super();
    this.customData = data;
    this.isCustom = true;
    this.id = id;
    this.label = label;
  }
}

4. 创建可扩展属性类

该类用于自定义请求、字段等属性,扩展性高。

FieldFormat 包下创建 Field.js,内容如下:

js 复制代码
import Base from "./Base";

/**
 * 字段,用以匹配后端字段
 */
export default class Field extends Base {
  constructor(serve, id, label, method, dataField) {
    super();
    this.serve = serve;
    this.id = id;
    this.label = label;
    if (method) {
      this.method = method;
    }
    if (dataField) {
      this.dataField = dataField;
    }
  }
}

5. 创建字典类

该类用于定义字典属性列表,用于匹配字典数据。

FieldFormat 包下创建 Dict.js,内容如下:

js 复制代码
import Base from "./Base";

/**
 * 字典,用以匹配后端字典
 */
export default class Dict extends Base {
  constructor(serve) {
    super();
    this.serve = serve;
    this.id = "dictValue";
    this.label = "dictLabel";
    this.isDict = true;
  }
}

6. 创建后端枚举类

该类用于定义枚举属性列表,用于匹配后端枚举数据。

FieldFormat 包下创建 Enum.js,内容如下:

js 复制代码
import Base from "./Base";

/**
 * 枚举,根据name字段匹配
 */
export default class Enum extends Base {
  constructor(serve) {
    super();
    this.id = "name";
    this.label = "description";
    this.isEnum = true;
    this.serve = serve;
  }
}

7. 创建格式化类型列表

创建 formatOptions.js,内容如下(仅提供部分参考,依个人需求变更):

js 复制代码
import * as vehicleTypeService from "@/api/bayonet/vehicleType";
import Enum from "./Enum";
import Dict from "./Dict";
import Field from "./Field";
import Custom from "./Custom";

// 枚举路径前缀
const pathBayonetPrefix = "com.jl15988.project.bayonet.enums.";
const pathApplicationPrefix = "com.jl15988.project.application.enums.";

/**
 * 字段格式化组件参数
 *
 * @param serve 请求地址或请求方法或枚举类型,请求方法可以是api中的,必须是Function: () => Promise格式
 * @param id 请求后的数据列表字段,用于匹配那一条数据
 * @param label 请求后的数据列表字段,用于自动格式化字段
 * @param method 请求方式,默认get
 * @param dataField 请求后的data字段,默认data
 * @param isEnum 是否枚举,开启将请求后端枚举
 * @param isDict 是否字典,开启将请求后端字典
 * @param isCustom 是否自定义,开启自定义数据模式
 * @param customData 自定义的数据
 * @param render 用于自定义渲染操作,参数为(data, list),data为当前数据项,list为全部数据列表
 */
export default {
  // 车辆类型。普通的拓展属性
  vehicleType: new Field(vehicleTypeService.getList, "vehicleTypeId", "name"),
  // 车辆类型(全路径)。自定义渲染的拓展属性
  vehicleTypeFull: new Field(vehicleTypeService.listAll, "vehicleTypeId", "name")
    .renders((data, list) => {
      // 这里可以通过 data、list 参数来完成个人需要的格式。下面是根据节点ID,拼完整的类型。
      if (!data || !data.name) {
        return "";
      }
      const names = [data.name];

      findParent(data);

      function findParent(row) {
        if (!row.parentId) {
          return;
        }
        const vehicleType = list.find(item => item.vehicleTypeId === row.parentId);
        if (vehicleType && vehicleType.name) {
          names.push(vehicleType.name);
        }
        if (vehicleType && list.filter(item => item.vehicleTypeId === vehicleType.parentId).length > 0) {
          findParent(vehicleType);
        }
      }

      names.reverse();
      return names.join("/");
    }),
  // 审批状态。枚举属性
  approvalStatusEnum: new Enum(pathApplicationPrefix + "ApprovalStatus")
    // 通过定义 tag 可以实现显示 el-tag 样式
    .tags({
      "10": "", // 待审核
      "20": "info",
      "30": "warning", // 企业驳回
      "40": "success", // 已通过
      "50": "warning", // 已驳回
      "60": "danger" // 停运
    }),
  // 车辆是否进入。字典属性
  vehicle_enter_status: new Dict("vehicle_enter_status")
    .tags({
      "0": "",
      "1": "success"
    }),
  // 异常车辆状态。自定义属性
  abnormalVehicleStatus: new Custom({
    "0": "正常",
    "1": "异常"
  }).tags({
    "0": "success",
    "1": "danger"
  })
}

8. 修改组件

将 index.vue 改为一下内容:

html 复制代码
<template>
  <span>
    <template v-if="!hasSlot && !tag && !tags && !tagTypes">{{ labelValue }}</template>
    <el-tag v-if="!hasSlot && labelValue && (tag || tags || tagTypes)" :type="tagType">{{ labelValue }}</el-tag>
    <slot></slot>
    <slot name="format" :data="data"></slot>
    <slot name="list" :list="list"></slot>
  </span>
</template>

<script>
import {cacheGet, cachePost} from '@/utils/request';
import formatOptions from "./formatOptions";

export default {
  name: "FieldFormat",
  props: {
    /**
     * 用于匹配的值
     */
    value: [String, Number],
    /**
     * 要格式化的类型
     */
    type: String,
    /**
     * 发起请求的额外参数
     */
    params: Object,
    /**
     * 没有匹配的数据时,代替显示的内容
     */
    alternate: String,
    /**
     * 关闭Tag标签样式
     */
    closeTag: Boolean,
    /**
     * 要显示的Tag标签样式(默认的为default),见Element文档
     */
    tag: String,
    /**
     * 按数据显示的Tag标签样式,数据值为key,样式为值
     */
    tags: Object
  },
  data() {
    return {
      enumUrl: 'common/utility/getEnumList',
      dictUrl: 'system/dict/data/dictType/',
      data: undefined,
      list: [],
      serve: undefined,
      id: undefined,
      label: undefined,
      method: 'get',
      dataField: 'data',
      isEnum: false,
      isDict: false,
      isCustom: false,
      customData: undefined,
      render: undefined,
      tagTypes: undefined
    }
  },
  computed: {
    fieldFormats() {
      // 获取vuex中缓存的数据
      return this.$store.state.fieldFormat.types;
    },
    hasSlot() {
      // 判断有没有插槽(默认插槽除外)
      return (this.$scopedSlots && (!!this.$scopedSlots.list || !!this.$scopedSlots.format))
        || (this.$slots && (!!this.$slots.list || !!this.$slots.format));
    },
    labelValue() {
      if (this.render) {
        return this.render(this.data, this.list);
      } else if (this.isCustom && !this.id) {
        return this.customData[this.value];
      } else if (this.data && this.label) {
        return this.data[this.label];
      } else {
        return this.alternate;
      }
    },
    tagType() {
      if (this.closeTag) {
        return "";
      }
      if (this.tag) {
        return this.tag;
      } else if (this.tags) {
        return this.tags[this.value];
      } else if (this.tagTypes) {
        return this.tagTypes[this.value];
      } else {
        return "";
      }
    }
  },
  watch: {
    type: {
      handler(n) {
        // 类型改变时重新获取数据
        this.getData();
      }
    },
    value: {
      handler(n) {
        // 值改变时重新解析
        this.format();
      }
    }
  },
  methods: {
    /**
     * 解析
     */
    format() {
      // 在列表中查找对应数据
      if (this.isCustom && this.id) {
        this.list = this.customData;
      }

      const list = this.list;
      if (list && list.length > 0) {
        this.data = list.find(datum => String(datum[this.id]) === String(this.value));
      }
    },
    /**
     * 获取参数
     * @returns {string|*}
     */
    getOption() {
      // 根据type获取option
      const option = formatOptions[this.type];
      // 赋值属性
      Object.assign(this.$data, option);
      return option.serve;
    },
    /**
     * 获取数据
     */
    async getData() {
      const serve = this.getOption();

      // 如果vuex中有当前类型缓存,则取缓存
      if (this.fieldFormats[this.type]) {
        this.list = this.fieldFormats[this.type];
        this.format();
        return;
      }

      if (serve instanceof Function) {
        // 如果serve类型为Function,则直接调用取值
        serve().then(res => {
          this.relRes(res);
        });
      } else {
        if (this.isDict) {
          await this.relDict();
        } else if (this.isEnum) {
          await this.relEnum();
        } else if (this.isCustom) {
          this.format();
        } else {
          let res;
          if (this.method === "post") {
            res = await cachePost(serve, this.params);
          } else {
            res = await cacheGet(serve, this.params);
          }
          this.relRes(res);
        }
      }
    },
    /**
     * 解析枚举
     */
    async relEnum() {
      const res = await cacheGet(this.enumUrl, {
        enumType: this.serve
      })
      this.relRes(res);
    },
    /**
     * 解析字典
     */
    async relDict() {
      const res = await cacheGet(this.dictUrl + this.serve);
      this.relRes(res);
    },
    /**
     * 解析结果
     */
    relRes(res) {
      let list = this.list = res[this.dataField];

      this.$store.commit("fieldFormat/ADD_TYPE", {
        type: this.type,
        value: list
      });

      this.format();
    }
  },
  created() {
    this.getData();
  }
}
</script>

9. 创建store缓存

为了降低请求频率,降低后端请求压力,我们添加 store 进行缓存获取的数据。

创建 fieldFormat.js,内容如下:

js 复制代码
export default {
  namespaced: true,
  state: {
    types: {}
  },
  mutations: {
    ADD_TYPE: (state, params) => {
      state.types[params.type] = params.value;
    }
  }
}

并添加到 store 的 modules 中:

js 复制代码
import fieldFormat from "./modules/fieldFormat";
const store = new Vuex.Store({
  modules: {
    fieldFormat
  },
  getters: {
    fieldFormat: state => state.fieldFormat.types
  }
})

export default store

10. 创建缓存请求

在表格渲染中,会创建多个相同类型的组件,由于第一次请求,缓存中没有该类型的数据,造成大量的请求,所以我们添加缓存请求,降低请求次数。

我们使用的使 axios.js,但其实普通的 ajax 也可以。

js 复制代码
const cacheMap = {};
// 响应拦截器
service.interceptors.response.use(res => {
  try {
    // 删除缓存,这里的 api 根据个人需求变更
    const baseApi = res.config.url.replace(process.env.VUE_APP_BASE_API, "");
    let api;
    if (res.config.method === 'get') {
      api = baseApi + JSON.stringify(res.config.params);
    } else {
      api = baseApi + JSON.stringify(res.config.data);
    }
    if (cacheMap.hasOwnProperty(api)) {
      delete cacheMap[api];
    }
  } catch (err) {
  }
}

/**
 * Get缓存请求
 */
export const cacheGet = async (api, params) => {
  if (api.indexOf("/") !== 0) {
    api = "/" + api;
  }
  const key = api + JSON.stringify(params);
  if (!cacheMap.hasOwnProperty(key)) {
    cacheMap[key] = service({
      url: api,
      method: 'get',
      params
    });
  }
  return cacheMap[key];
}

/**
 * Post缓存请求
 */
export const cachePost = async (api, data) => {
  if (api.indexOf("/") !== 0) {
    api = "/" + api;
  }
  const key = api + JSON.stringify(data);
  if (cacheMap.hasOwnProperty(key)) {
    cacheMap[key] = service({
      url: api,
      method: 'post',
      data
    });
  }
  return cacheMap[key];
}

11. 添加全局组件

为了能够使组件能够全局调用,而不用单独引入,我们在 main.js 中引入,并挂载到全局

js 复制代码
import FieldFormat from "@/components/FieldFormat";
Vue.component('FieldFormat', FieldFormat);

12. 说明

属性类可自行创建,也可以直接使用 JSON 格式,创建属性类是为了能够更方便、快捷的使用。

13. 属性

1. 类属性

属性 类型 说明
serve String 或 Function 请求地址或请求方法或枚举类型,请求方法可以是api中的,必须是Function: () => Promise格式
id String 请求后的数据列表字段,用于匹配那一条数据
label String 请求后的数据列表字段,用于自动格式化字段
method String 请求方式,默认get
dataField String 请求后的data字段,默认data
isEnum Boolean 是否枚举,开启将请求后端枚举
isDict Boolean 是否字典,开启将请求后端字典
isCustom Boolean 是否自定义,开启自定义数据模式
customData Object 或 Array 自定义的数据
render Function 用于自定义渲染操作,参数为(data, list),data为当前数据项,list为全部数据列表

2. 组件属性

属性 类型 说明
value String 或 Number 用于匹配的值
type String 要格式化的类型
params Object 发起请求的额外参数
alternate String 没有匹配的数据时,代替显示的内容
closeTag Boolean 关闭Tag标签样式
tag String 要显示的Tag标签样式(默认的为default),见Element文档
tags Object 按数据显示的Tag标签样式,数据值为key,样式为值

14. 使用

1. 格式化

在需要格式化的地方,使用组件 field-format,value为已知数据值, type 为 formatOptions 中添加的名称,另外还有 params 字段用于请求自定义传参

html 复制代码
<field-format :value="form.vehicleType" type="vehicleType"></field-format>

2. 自定义插槽

可以使用插槽实现更多场景的功能,如

html 复制代码
<field-format :value="form.vehicleType" type="vehicleType">
  <template #format="{data}">{{ data.name }}</template>
</field-format>

3. 遍历

或者获取所有列表,用于遍历

html 复制代码
<field-format type="vehicleType">
    <template #list="{list}">
      <el-select v-model="form.vehicleType">
        <el-option
          v-for="item in list"
          :label="item.name"
          :value="item.vehicleTypeId"
          :key="item.vehicleTypeId"
        ></el-option>
      </el-select>
    </template>
  </field-format>
</el-form-item>

4. 默认插槽

用以自定义追加数据

相关推荐
恋猫de小郭8 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅15 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606115 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了15 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅15 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅16 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅16 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment16 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅17 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊17 小时前
jwt介绍
前端