枚举值组件EnumBase
(实现展示数据的时候提供tag样式,也可以变成下拉选择)
-
AllowSelect
判断是要下拉选择还是展示tag标签 (所有prop要使用的时候尽量通过computed进行,避免直接修改prop值)<div :class="[AllowSelect?'auto-width':'']"> <tr-select v-if="AllowSelect" ... /> <template v-else> ... </template> </div>
-
传入的枚举数据需要处理。使用传入过滤方法或者默认方法进行过滤,将源数据转换成数组形式,然后使用数组的
filter
方法过滤,需确保返回结果是数组。Options: { get() { const defaultFilter = (value) => value const filterMethod = this.optionFilter ?? defaultFilter const sourceOptions = this.EnumData?.toArray() || [] const filterResult = sourceOptions.filter(filterMethod) const filterResultIsArray = Array.isArray(filterResult) return filterResultIsArray ? filterResult : [] } }
-
对于不允许选择,存在直接展示和tag展示。tag的样式从prop传入数组中获取。 这时候需要注意
label
的长度来控制展示效果(getStringLength方法
)TagStyle: { get() { let widestLabelLength = 0 this.Options.forEach((option) => { const labelWidth = this.getStringLength(option.label) widestLabelLength = widestLabelLength < labelWidth ? labelWidth : widestLabelLength }) const defaultTagWidth = `${widestLabelLength * 5 + 10}px` return `width:${this.tagWidth ?? defaultTagWidth};` } },
<template>EnumBase
代码:<tr-select v-if="AllowSelect" :data.sync="Data" :options="Options" v-bind="$attrs" width="100%" v-on="$listeners" />
</template> <script> import TrSelect from '@/components/Select/Select.vue'<template v-else> <el-tag v-if="UseTag && LabelVisible" :type="Type" :disable-transitions="true" v-bind="$attrs" v-on="$listeners" > <div class="tag-content" :style="TagStyle"> <span>{{ Label }}</span> </div> </el-tag> <span v-else>{{ Label }}</span> </template> </div>
// #region 值转换函数
// #endregion
export default {
components: {
TrSelect
},
model: {
prop: 'data',
event: 'update:data'
},
props: {
/** 绑定值 /
data: {
type: [String, Number, Array],
default: undefined
},
/* 是否允许选择 /
allowSelect: {
type: Boolean,
default: true
},
/* 是否使用tag装饰,仅在allowSelect为false时有效 */
useTag: {
type: Boolean,
default: true
},/** Tag的宽度 */ tagWidth: { type: String, default: undefined }, /** options的过滤函数 */ optionFilter: { type: Function, default: undefined } }, data() { return { myData: undefined, enumData: undefined, EnumData: undefined } }, computed: { Data: { get() { return this.data ?? this.myData }, set(val) { this.myData = val this.$emit('update:data', val) } }, AllowSelect: { get() { return this.allowSelect } }, UseTag: { get() { return !this.allowSelect && this.useTag } }, TagStyle: { get() { let widestLabelLength = 0 this.Options.forEach((option) => { const labelWidth = this.getStringLength(option.label) widestLabelLength = widestLabelLength < labelWidth ? labelWidth : widestLabelLength }) // console.info(widestLabelLength) const defaultTagWidth = `${widestLabelLength * 5 + 10}px` return `width:${this.tagWidth ?? defaultTagWidth};` } }, Label: { get() { // console.info(this.Data) // console.info(this.valueToLabel(this.Data)) return this.valueToLabel(this.Data) } }, LabelVisible: { get() { return this.Label !== '' } }, Type: { get() { return this.valueToType(this.Data) } }, Options: { get() { const defaultFilter = (value) => value const filterMethod = this.optionFilter ?? defaultFilter const sourceOptions = this.EnumData?.toArray() || [] const filterResult = sourceOptions.filter(filterMethod) const filterResultIsArray = Array.isArray(filterResult) return filterResultIsArray ? filterResult : [] } } }, watch: { enumData: { deep: true, handler(newValue) { this.EnumData = newValue } }, data: { handler(value) { this.myData = value } } }, mounted() { }, methods: { valueToLabel(value) { // console.info(this.EnumData) const label = this.EnumData?.getItemByValue(value)?.label || '' return label }, valueToType(value) { const type = this.EnumData?.getItemByValue(value)?.type || '' return type }, getStringLength(str) { if (str === undefined) { return 0 } const chineseCharacterLength = 2 const englishCharacterLength = 1 const chinese = str.match(/[\u4e00-\u9fa5]/g) const chineseCount = chinese ? chinese.length : 0 const englishCount = str.length - chineseCount return chineseCount * chineseCharacterLength + englishCount * englishCharacterLength } }
}
<style lang="scss" scoped> .tag-content { display: flex; justify-content: center; } </style>
</script>TrSelect
封装TrSelect
代码<template> <div class="tr-select" :style="WidthStyle"> <el-select ref="selectRef" v-model="Data" v-bind="$attrs" :filterable="Filterable" :remote="remote" :placeholder="placeholder || $t('qing-xuan-ze')" :remote-method="remoteSearch" :loading="loading" :clearable="clearable" :multiple="multiple" :collapse-tags="true" :disabled="disabled" :default-first-option="true" style="width: 100%" @focus="handleFocus" @change="handleChange" @remove-tag="handleRemoveTag" @clear="handleClear" @blur="handleBlur" > <el-option v-for="(opt, index) in Options" :key="index" :label="opt.label" :value="opt.value" :disabled="opt.disabled || false" /> </el-select> </div> </template> <script> export default { name: 'TrSelect', components: {}, model: { prop: 'data', event: 'update:data' }, props: { data: { type: [String, Number, Array], default: '' }, /** * getDataFunction(done,data,searchWord) * 当调用done(newOptions)时,会把newOptions内的选项传递给组件 * 当设置remote为True时,用户输入的内容会随searchWord传递 * 为了能够解决远程搜索的反显问题,当remote为true时,会把当前组件绑定的data也传递进去 * 这样可以通过该参数搜索后台,并进行反显 * */ getDataFunction: { type: Function, default: undefined }, /** 设置默认的选项,该选项不会被getDataFunction传递的选项覆盖 */ options: { type: Array, default: () => [ // { // label: '选项1', // value: 1 // }, // { // label: '选项2', // value: 2 // }, // { // label: '选项3', // value: 3 // } ] }, /** * 刷新选项的方式: 1:created时刷新(默认值) 2.mounted时刷新 4.focus时刷新 8.refreshFlag更改时触发 * 可以任意组合上述四项,例如传递3,代表了created和mounted时会刷新,传递5,代表了created和focus时会刷新 */ refreshMode: { type: Number, default: 1 }, /** 使用deepWatch监听 */ refreshFlag: { type: [Boolean, String, Object, Array, Number], default: undefined }, refreshDeepWatch: { type: Boolean, default: false }, /** 是否自动选中选项中的第一项,优先级低于其他选中配置 */ autoSelect: { type: Boolean, default: false }, autoSelectFirstOne: { type: Boolean, default: false }, /** 是否根据label值默认选中选项 */ defaultSelectByLabel: { type: [String, Number, Array], default: undefined }, /** 是否根据value值默认选中选项 */ defaultSelectByValue: { type: [String, Number, Array], default: undefined }, /** 可以通过'100px'设定固定宽度,也可以通过'100%'设置百分比宽度,默认值为240px,与el-select一致*/ width: { type: String, default: undefined }, placeholder: { type: String, default: undefined }, /** 需要过滤时设为true */ filterable: { type: Boolean, default: false }, /** 需要启用远程功能时,设为true */ remote: { type: Boolean, default: false }, /** 当用户输入完内容后,间隔多久后开始调用getDataFunction,单位毫秒 */ remoteDelay: { type: Number, default: 300 }, /** 请勿在这里传递,该属性仅仅起到阻断作用 */ remoteMethod: { type: Function, default: undefined }, /** 远程搜索是否允许空的字符串 */ remoteSearhAllowEmptyString: { type: Boolean, default: false }, /** 是否可清除 */ clearable: { type: Boolean, default: true }, /** 是否多选 */ multiple: { type: Boolean, default: false }, /** 是否不可更改 */ disabled: { type: Boolean, default: false }, /** 当前选择的内容的全部数据 */ currentSelect: { type: [Array, String, Object, Number, Boolean], default: undefined } }, data() { return { loading: false, myOptions: [], remoteSearchKeyword: undefined, myRemoteTask: null, myOptionsDictionary: {}, disabledWatchEmit: false } }, computed: { Data: { get() { return this.data }, set(val) { // console.info('update:data', val) this.$emit('update:currentSelect', this.myOptionsDictionary[val]) this.$emit('update:data', val) this.$emit('change', val) this.disabledWatchEmit = true setTimeout(() => { this.disabledWatchEmit = false }, 200) // console.info(this.myOptionsDictionary) // console.info('设置了', val) } }, Options: { get() { var temp = [] // 合并默认选项 if (typeof this.options === 'object') { Object.assign(temp, this.options) } // 合并远程选项 Object.assign(temp, this.myOptions) // 建立value和项对应的字典 // eslint-disable-next-line vue/no-side-effects-in-computed-properties this.myOptionsDictionary = {} Object.keys(temp).forEach((key) => { this.myOptionsDictionary[temp[key].value] = temp[key] }) return temp }, set(val) { if (typeof val === 'object') { this.myOptions = val // 通过访get方法,建立字典,下一行不可删除 var test = this.Options } } }, RefreshWhenCreated: { get() { return (this.refreshMode & 1) === 1 } }, RefreshWhenMounted: { get() { return (this.refreshMode & 2) === 2 } }, RefreshWhenFocus: { get() { // console.info('计算') // console.info(this.refreshMode) // console.info(this.refreshMode & 4) return (this.refreshMode & 4) === 4 } }, RefreshWhenRefreshFlagChange: { get() { return (this.refreshMode & 8) === 8 } }, Filterable: { get() { // 当启用了远程搜索时,直接返回true,否则返回filterable if (this.remote === true) { return true } else { return this.filterable } } }, WidthStyle: { get() { if (this.width === undefined) { return 'display:inline-block;position:relative;' } else { return `width:${this.width};` } } } }, watch: { refreshFlag: { deep: true, handler(newValue, oldValue) { if (newValue === undefined) { return } // 在调用之前,需要将loading重置,否则不会重新刷新方法 this.loading = false this.refreshOptions() }, immediate: false }, Options: { deep: true, handler(newValue, oldValue) { // 当选项更改时,要考虑绑定值是否与选项相契合,如果绑定值为空,则要考虑自动选中 if ([undefined, null, ''].includes(this.Data)) { setTimeout(() => { this.doAutoSelect(newValue) }, 10) } else { this.fitCurrentValue(newValue) } } }, Data: { handler(value) { if (this.disabledWatchEmit) return this.$emit('change', value) } } }, created() { if (this.RefreshWhenCreated) { this.refreshOptions() } }, mounted() { if (this.RefreshWhenMounted) { this.refreshOptions() } // console.info(this.$listeners) }, methods: { /** 远程搜索,防抖 */ remoteSearch(keyword) { // console.info(keyword) if ([undefined, null].includes(keyword)) { return } if (typeof keyword !== 'string') { return } const word = keyword.trim() if (word === '' && !this.remoteSearhAllowEmptyString) { return } // 取消掉之前的任务 if (this.myRemoteTask !== null) { clearTimeout(this.myRemoteTask) } // 清空绑定值 // 重新建立新任务 this.myRemoteTask = setTimeout(() => { this.refreshOptions(keyword) this.myRemoteTask = null }, this.remoteDelay) }, /** 刷新选项 */ refreshOptions(keyword) { if (!this.loading) { // debugger this.loading = true var value = null if (this.remote) { value = JSON.parse(JSON.stringify(this.Data)) } // //console.info(typeof (this.getDataFunction)) // //console.info(12) if ( this.getDataFunction !== undefined && typeof this.getDataFunction === 'function' ) { this.getDataFunction(this.appendOptions, value, keyword) } else { this.loading = false } } }, /** 添加远程搜索的选项 */ appendOptions(newOptions) { if (this.loading) { // debugger this.loading = false this.Options = newOptions } }, /** 绑定值更改 */ handleChange(data) { // console.info(this.myOptionsDictionary) // console.info(data) }, /** 被聚焦 */ handleFocus() { if (this.RefreshWhenFocus) { // console.info(11) this.refreshOptions() } this.$emit('focus') }, /** 多选项被移除 */ handleRemoveTag(value) { this.$emit('removeTag', value) }, /** 绑定值被清除 */ handleClear() { // this.$emit('update:currentSelect', undefined) this.$emit('clear') }, /** 组件失去焦点 */ handleBlur(event) { // console.info('失去焦点') // console.info(event) this.$emit('blur', event) }, /** */ fitCurrentValue(newOptions) { newOptions.forEach((opt) => { if (opt.value === this.Data) { this.Data = opt.value } }) }, /** 执行自动选择 */ doAutoSelect(newOptions) { // 只有一个选项时,默认选中,和有多个选项时,默认选中第一个是相同的 // 如果指定了选择的标准(按照value值或者按照label值,优先级value大于label,),则按照标准进行选择 // 选择了后要触发值更改事件 // console.info('执行自动选择') // const newOptions = this.Options const valueList = [] const labelList = [] if (this.autoSelectFirstOne && newOptions.length >= 1) { valueList.push(newOptions[0].value) } try { // 考虑value选中(单选时优先级高于label) // 考虑label选中 // 考虑第一项自动选中,优先级最低 // 遍历选项,根据条件找到所有的满足条件的项 newOptions.forEach((opt) => { if (this.defaultSelectByValue !== undefined) { if ( ['string', 'number'].includes(typeof this.defaultSelectByValue) ) { // 简单类型 if (this.defaultSelectByValue === opt.value) { valueList.push(opt.value) } } else if (this.defaultSelectByValue instanceof Array) { // 数组类型 if (this.defaultSelectByValue.includes(opt.value)) { valueList.push(opt.value) } } } if (this.defaultSelectByLabel !== undefined) { if (['string', 'number'].includes(typeof (this.defaultSelectByLabel))) { // 简单类型 if (this.defaultSelectByLabel === opt.label) { labelList.push(opt.value) } } else if (this.defaultSelectByLabel instanceof Array) { // 数组类型 if (this.defaultSelectByLabel.includes(opt.label)) { labelList.push(opt.value) } } } }) const a = new Set(labelList) const b = new Set(valueList) const multiSelect = Array.from(new Set([...a, ...b])) const multiData = multiSelect.length >= 1 ? multiSelect : '' const firstValue = newOptions.length === 1 ? [newOptions[0].value] : [] const singleSelect = [].concat(firstValue, labelList, valueList) const singleData = singleSelect.length >= 1 ? singleSelect[singleSelect.length - 1] : '' const newData = this.multiple === true ? multiData : singleData // 设置值 if (newData !== '') { this.Data = newData this.$refs.selectRef.$emit('change', newData) this.$refs.selectRef.blur() } } catch (err) { console.error(err) } }, clearOptions() { this.Options = [] } } } </script> <style lang="scss" scoped> </style>
使用方法:
定义js文件
import { EsEnum } from '@/components/utils' export const LogsType = new EsEnum([ { code: 'ESign', value: 1, label: '电子签名', type: 'success' }, { code: 'User', label: '用户操作, type: 'primary' } ])
定义vue文件
<script> import EnumBase from '@/components/Template/EnumBase' import { LogsType } from './LogsType.js' export default { extends: EnumBase, created() { this.enumData = LogsType } } </script>
EsEnum方法的封装
// #region 名称转换 /** * 函数将name转换为 camelCase, CamelCase, kebab-case 的形式 * @param name: 待格式化的名称 * @param pattern: 模式,若传入number类型,则camelCase为1,其余按顺序递增 */ export function NameFormat(name, pattern) { const patternRange = ['camelCase', 'CamelCase', 'kebab-case'] let selectedPattern = null if (typeof pattern === 'number') { let index = pattern < 1 ? 1 : pattern index = index > patternRange.length ? patternRange.length : index selectedPattern = patternRange[index - 1] } else { if (patternRange.indexOf(pattern) === -1) { selectedPattern = patternRange[0] } else { selectedPattern = pattern } } switch (selectedPattern) { case 'camelCase': return camelCase(name) case 'CamelCase': return CamelCase(name) case 'kebab-case': return kebabCase(name) } } /** * 将输入的字符串转换为kebab-case * @param str 输入的字符串 * @returns 转换后的结果 */ function kebabCase(str) { return str .replace(/([^A-Za-z0-9]+)/g, '-') // 任意非数字字母的连续字符 => - .replace(/^([0-9-]+)([a-zA-Z])/g, '$2') // 去掉单词前的所有非字母字符 .replace(/([a-zA-z0-9])(-?)$/g, '$1') // 去掉单词后的所有非字母、非数字字符 .replace(/([a-z])([A-Z])/g, '$1-$2')// aA=>a-A .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')// AAAa=>AA-Aa .replace(/([0-9])([a-zA-Z])/g, '$1-$2') // V9a=>V9-a .toLowerCase() } /** * 将输入的字符串转为CamelCase * @param str 输入的字符串 * @returns 转换后的结果 */ function CamelCase(str) { return kebabCase(str) .replace(/(.)/, '-$1') .replace(/(-[a-z])/g, (str) => str[1].toUpperCase()) } /** * 将输入的字符串转为camelCase * @param str 输入的字符串 * @returns 转换后的结果 */ function camelCase(str) { return CamelCase(str) .replace(/(.)/, (str) => str.toLowerCase()) } // const testData = ['goodMorning', 'GoodMorning', '-458---goodMorning....', 'good--Morning', 'goodMORNINGEveryday'] // for (const str of testData) { // console.log(str) // console.log(kebabCase(str)) // console.log(CamelCase(str)) // console.log(camelCase(str)) // console.log('') // } // #endregion /** * 该函数考虑expression和method(params) * @param {*} expression 返回布尔值的表达式 * @param {*} valueDefault 默认值 * @param {Function} method 返回布尔值的函数 * @param {Object|Array} params 函数的入参 * @returns */ export function BoolValueHandle(expression, valueDefault, method, params) { if (method === undefined) { if (expression === undefined) { return valueDefault } else { return expression } } else { var temp = method(params) if (temp !== undefined && typeof (temp) === 'boolean') { return temp } else { console.warn('方法应该返回boolean类型的值') if (expression === undefined) { return valueDefault } else { return expression } } } } /** * 该函数将考虑valueOrFunction的类型是function还是其他。 * 为undefined,返回 valueDefault; * 为function时,返回 function(params) ?? valueDefault * 其他情况,返回 value * @param {*} valueOrFunction 值或者函数 * @param {*} valueDefault 值或函数为undefined时的默认值 * @param {*} params 传递给函数的参数 * @returns */ export function ValueHandle(valueOrFunction, valueDefault, params) { // console.info(valueOrFunction) if (valueOrFunction === undefined) { return valueDefault } if (typeof valueOrFunction !== 'function') { const value = valueOrFunction return value } // console.info(valueOrFunction) const method = valueOrFunction const methodReturns = method(params) // console.info(methodReturns) return methodReturns ?? valueDefault } /** * 生成枚举型的Object * @param {*} arr 枚举的设定值。例如[['red',0],['green',1]],最终可以通过.red .green获得对应值0 和 1 * @returns 枚举型 */ export function MakeEnum(arr) { const obj = {} if (!Array.isArray(arr)) { throw new Error('arr 不是 Array') } arr.forEach((element, index) => { if (typeof element !== 'object' && typeof element !== 'string' && !Array.isArray(element)) { throw new Error('arr的元素只允许是object array string 类型的') } let tempObject = {} if (Array.isArray(element)) { let [code, value] = element if (code === undefined) { throw new Error('arr的元素若为Array类型,至少需要有一个元素') } value = value ?? index const temp = { code, value } tempObject = Object.assign({}, temp) } else if (typeof element === 'string') { const code = element const value = index const temp = { code, value } tempObject = Object.assign({}, temp) } else { let { code, value } = element if (code === undefined) { throw new Error('arr的元素若为Object类型,至少需要有code属性') } value = value ?? index const temp = { code, value } tempObject = Object.assign({}, temp) } // 编码若出现重复,后者覆盖前者 obj[tempObject.code] = tempObject.value }) return Object.freeze(obj) } /** * 传入[item0,item1,item2,item3,...],其中item至少有code和value两个属性 * 返回一个枚举类,提供code到value,value到item的映射,并可以通过迭代器获得 * [item0,item1,item2,item3,...] */ export class EsEnum { constructor(arr, startValue = 1) { if (!Array.isArray(arr)) { throw new Error('arr 不是 Array') } let nextValue = startValue // 建立value与index的映射关系 this.indexObject = {} this.codeObject = {} // 建立code到value的映射关系的同时,生成共迭代器访问的Array this.data = arr.map((item, index) => { const { code, value } = item // code 不允许为undefined if ([undefined, null, ''].includes(code)) { throw new Error('arr的元素的code 不允许为 空或null或undefined') } const useValue = value ?? nextValue nextValue = (value ?? nextValue) + 1 this[code] = useValue this.indexObject[useValue] = index this.codeObject[item.code] = index // 使用浅拷贝 return Object.assign({}, item, { value: useValue }) }) } /** * 默认迭代器 */ * [Symbol.iterator]() { for (const item of this.data) { yield item } } /** * 返回array */ toArray() { return [...this.data] } /** * 使用value查找与value匹配的枚举项配的结果 * @param {*} value 待匹配的value值 * @returns 返回item或者undefined */ getItemByValue(value) { const index = this.indexObject[value] return this.data[index] } /** * 使用code查找与code匹配的枚举项配的结果 * @param {*} code 待匹配的code值 * @returns 返回item或者undefined */ getItemByCode(code) { const index = this.codeObject[code] return this.data[index] } } /** * 该函数处理window.addEventListener事件传递的event,从event中提取按键的字符串(key)和ASC2码(keyCode) * @param {*} event addEventListener * @returns {key ,keyCode} */ export function KeyboardListener(event) { const e = event || window.event || arguments.callee.caller.arguments[0] if (!e) return const { key, keyCode } = e // console.log(key, keyCode) return { key, keyCode } } /** * 获取当前系统颜色 * @param {String} type 类型 * @returns 颜色代码 */ export function GetColorByType(type) { switch (type) { case '': case 'primary': return '#409EFF' case 'success': return '#67C23A' case 'danger': return '#F56C6C' case 'warning': return '#E6A23C' case 'info': default: return '#909399' } }
下拉框使用
<LogsType v-model="xxx"/> //引用.vue文件。此时会提供一个下拉框(数据展示的是label值),选中数据后xxx获取的是value值,也可以直接用js通过xxx=LogsType.[code]来赋值
tag使用
<LogsType v-model="xxx" :allow-select="false" />
直接使用
<LogsType v-model="xxx" :allow-select="false" :use-tag="false" /> //增加use-tag属性
$t和i18n是多语言处理,可以看前面的博客