场景: 对于一些全局公用的状态,或是字典,甚至是后端枚举,为了方便维护,我们应尽量使它们全局公用,但我们在开发往往会忽略这个问题,总想着后面再改,可随着项目的不断推进,我们往往都视之不理。
功能: 解决vue项目中字段、字典、状态类全局维护问题。
优势: 一次配置全局公用、可单独变更、可自定义、低请求、方便、快捷。
特点: 组件化、全局化、公用化。
在 components
下创建 FieldFormat
包,然后在该包下创建 index.vue
该类为所有属性类的基类,也用以规范属性列表。
在 FieldFormat
包下创建 Base.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;
}
}
该类为自定义数据属性,可传入自定义的属性用于匹配字段。
在 FieldFormat
包下创建 Custom.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;
}
}
该类用于自定义请求、字段等属性,扩展性高。
在 FieldFormat
包下创建 Field.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;
}
}
}
该类用于定义字典属性列表,用于匹配字典数据。
在 FieldFormat
包下创建 Dict.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;
}
}
该类用于定义枚举属性列表,用于匹配后端枚举数据。
在 FieldFormat
包下创建 Enum.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;
}
}
创建 formatOptions.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"
})
}
将 index.vue 改为一下内容:
<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>
为了降低请求频率,降低后端请求压力,我们添加 store 进行缓存获取的数据。
创建 fieldFormat.js,内容如下:
export default {
namespaced: true,
state: {
types: {}
},
mutations: {
ADD_TYPE: (state, params) => {
state.types[params.type] = params.value;
}
}
}
并添加到 store 的 modules 中:
import fieldFormat from "./modules/fieldFormat";
const store = new Vuex.Store({
modules: {
fieldFormat
},
getters: {
fieldFormat: state => state.fieldFormat.types
}
})
export default store
在表格渲染中,会创建多个相同类型的组件,由于第一次请求,缓存中没有该类型的数据,造成大量的请求,所以我们添加缓存请求,降低请求次数。
我们使用的使 axios.js,但其实普通的 ajax 也可以。
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];
}
为了能够使组件能够全局调用,而不用单独引入,我们在 main.js 中引入,并挂载到全局
import FieldFormat from "@/components/FieldFormat";
Vue.component('FieldFormat', FieldFormat);
属性类可自行创建,也可以直接使用 JSON 格式,创建属性类是为了能够更方便、快捷的使用。
属性 | 类型 | 说明 |
---|---|---|
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为全部数据列表 |
属性 | 类型 | 说明 |
---|---|---|
value | String 或 Number | 用于匹配的值 |
type | String | 要格式化的类型 |
params | Object | 发起请求的额外参数 |
alternate | String | 没有匹配的数据时,代替显示的内容 |
closeTag | Boolean | 关闭Tag标签样式 |
tag | String | 要显示的Tag标签样式(默认的为default),见Element文档 |
tags | Object | 按数据显示的Tag标签样式,数据值为key,样式为值 |
在需要格式化的地方,使用组件 field-format
,value为已知数据值, type 为 formatOptions 中添加的名称,另外还有 params 字段用于请求自定义传参
<field-format :value="form.vehicleType" type="vehicleType">field-format>
可以使用插槽实现更多场景的功能,如
<field-format :value="form.vehicleType" type="vehicleType">
<template #format="{data}">{{ data.name }}template>
field-format>
或者获取所有列表,用于遍历
<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>
用以自定义追加数据