😎优化字典组件、下拉组件
This commit is contained in:
parent
0bac43bc87
commit
41b8b361cd
@ -99,7 +99,6 @@ watch(
|
|||||||
() => props.data,
|
() => props.data,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
state.data = newValue ?? [];
|
state.data = newValue ?? [];
|
||||||
console.log(state.data);
|
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -112,7 +112,7 @@ const props = defineProps({
|
|||||||
*/
|
*/
|
||||||
dropdownHeight: {
|
dropdownHeight: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '400px',
|
default: '550px',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 占位符
|
* 占位符
|
||||||
@ -128,6 +128,19 @@ const props = defineProps({
|
|||||||
type: Array<any>,
|
type: Array<any>,
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 查询表单高度偏移量
|
||||||
|
*/
|
||||||
|
queryHeightOffset: {
|
||||||
|
type: Number,
|
||||||
|
default: 35,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 查询表单标签宽度
|
||||||
|
*/
|
||||||
|
queryLabelWidth: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 查询参数
|
* 查询参数
|
||||||
*/
|
*/
|
||||||
@ -341,6 +354,7 @@ defineExpose({
|
|||||||
:remote-method="remoteMethod"
|
:remote-method="remoteMethod"
|
||||||
:default-first-option="allowCreate"
|
:default-first-option="allowCreate"
|
||||||
@visible-change="selectVisibleChange"
|
@visible-change="selectVisibleChange"
|
||||||
|
:style="{ width: dropdownWidth }"
|
||||||
popper-class="popper-class"
|
popper-class="popper-class"
|
||||||
ref="selectRef"
|
ref="selectRef"
|
||||||
remote-show-suffix
|
remote-show-suffix
|
||||||
@ -355,14 +369,14 @@ defineExpose({
|
|||||||
|
|
||||||
<!-- 下拉框内容区域 -->
|
<!-- 下拉框内容区域 -->
|
||||||
<div class="w100" v-loading="state.loading">
|
<div class="w100" v-loading="state.loading">
|
||||||
<el-form :model="state.tableQuery" v-if="$slots.queryForm" class="mg5 query-form" @click.stop>
|
<el-form :model="state.tableQuery" v-if="$slots.queryForm" class="mg5 query-form" :label-width="queryLabelWidth ?? ''" @click.stop>
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<!-- 查询表单插槽内容 -->
|
<!-- 查询表单插槽内容 -->
|
||||||
<slot name="queryForm" :query="state.tableQuery" :handleQuery="handleQuery"></slot>
|
<slot name="queryForm" :query="state.tableQuery" :handleQuery="handleQuery"></slot>
|
||||||
<!-- 查询和重置按钮 -->
|
<!-- 查询和重置按钮 -->
|
||||||
<el-button-group style="position: absolute; right: 10px">
|
<el-button-group style="position: absolute; right: 10px">
|
||||||
<el-button type="primary" icon="ele-Search" @click="remoteMethod(state.tableQuery)"> 查询 </el-button>
|
<el-button type="primary" icon="ele-Search" @click="remoteMethod(state.tableQuery)" v-reclick="1000"> 查询 </el-button>
|
||||||
<el-button icon="ele-Refresh" @click="resetQuery"> 重置 </el-button>
|
<el-button icon="ele-Refresh" @click="resetQuery" v-reclick="1000"> 重置 </el-button>
|
||||||
</el-button-group>
|
</el-button-group>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -372,8 +386,7 @@ defineExpose({
|
|||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
@row-click="handleChange"
|
@row-click="handleChange"
|
||||||
:data="state.tableData?.items ?? []"
|
:data="state.tableData?.items ?? []"
|
||||||
:height="`calc(${dropdownHeight} - 175px${$slots.queryForm ? ' - 35px' : ''}${state.tableQuery[keywordProp] && allowCreate ? ' - 35px' : ''})`"
|
:height="`calc(${dropdownHeight} - 175px${$slots.queryForm ? ` - ${queryHeightOffset}px` : ''}${state.tableQuery[keywordProp] && allowCreate ? ` - ${queryHeightOffset}px` : ''})`"
|
||||||
:style="{ width: dropdownWidth }"
|
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
>
|
>
|
||||||
<template #empty><el-empty :image-size="25" /></template>
|
<template #empty><el-empty :image-size="25" /></template>
|
||||||
@ -409,4 +422,19 @@ defineExpose({
|
|||||||
.popper-class {
|
.popper-class {
|
||||||
min-width: 400px !important;
|
min-width: 400px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 组合多个深度选择器 */
|
||||||
|
:deep(.popper-class) :deep(.el-select-dropdown__wrap) {
|
||||||
|
max-height: 600px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.popper-class .el-select-dropdown__wrap {
|
||||||
|
max-height: 450px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 或者使用属性选择器 */
|
||||||
|
.el-select-dropdown__wrap[max-height] {
|
||||||
|
max-height: 450px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -36,13 +36,13 @@ type TagType = (typeof TAG_TYPES)[number];
|
|||||||
* @property {string|number} [value] - 值
|
* @property {string|number} [value] - 值
|
||||||
*/
|
*/
|
||||||
interface DictItem {
|
interface DictItem {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
tagType?: TagType;
|
tagType?: TagType;
|
||||||
styleSetting?: string;
|
styleSetting?: string;
|
||||||
classSetting?: string;
|
classSetting?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,8 +52,8 @@ interface DictItem {
|
|||||||
* @property {Array<string|number>} excludes - 被互斥的选项值列表
|
* @property {Array<string|number>} excludes - 被互斥的选项值列表
|
||||||
*/
|
*/
|
||||||
interface MutexConfig {
|
interface MutexConfig {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
excludes: (string | number)[];
|
excludes: (string | number)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,8 +63,8 @@ interface MutexConfig {
|
|||||||
* @property {string} Comma - 逗号分隔模式,如'1,2,3'
|
* @property {string} Comma - 逗号分隔模式,如'1,2,3'
|
||||||
*/
|
*/
|
||||||
const MultipleModel = {
|
const MultipleModel = {
|
||||||
Array: 'array',
|
Array: 'array',
|
||||||
Comma: 'comma',
|
Comma: 'comma',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 多选值模式枚举类型
|
// 多选值模式枚举类型
|
||||||
@ -77,7 +77,7 @@ type MultipleModelType = (typeof MultipleModel)[keyof typeof MultipleModel];
|
|||||||
* @returns {value is RenderType} - 是否为合法的渲染类型
|
* @returns {value is RenderType} - 是否为合法的渲染类型
|
||||||
*/
|
*/
|
||||||
function isRenderType(value: any): value is RenderType {
|
function isRenderType(value: any): value is RenderType {
|
||||||
return RENDER_TYPES.includes(value);
|
return RENDER_TYPES.includes(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,7 +87,7 @@ function isRenderType(value: any): value is RenderType {
|
|||||||
* @returns {value is MultipleModel} - 是否为合法的多选模式
|
* @returns {value is MultipleModel} - 是否为合法的多选模式
|
||||||
*/
|
*/
|
||||||
function isMultipleModel(value: any): value is MultipleModelType {
|
function isMultipleModel(value: any): value is MultipleModelType {
|
||||||
return Object.values(MultipleModel).includes(value);
|
return Object.values(MultipleModel).includes(value);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -103,147 +103,158 @@ const emit = defineEmits(['update:modelValue', 'change']);
|
|||||||
* 组件属性定义
|
* 组件属性定义
|
||||||
*/
|
*/
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**
|
/**
|
||||||
* 绑定值,支持多种类型
|
* 绑定值,支持多种类型
|
||||||
* @type {string|number|boolean|Array|null}
|
* @type {string|number|boolean|Array|null}
|
||||||
* @required
|
* @required
|
||||||
* @example
|
* @example
|
||||||
* // 单选选择器
|
* // 单选选择器
|
||||||
* <g-sys-dict v-model="selectedValue" code="gender" renderAs="select" />
|
* <g-sys-dict v-model="selectedValue" code="gender" renderAs="select" />
|
||||||
*
|
*
|
||||||
* // 多选选择器(数组模式)
|
* // 多选选择器(数组模式)
|
||||||
* <g-sys-dict v-model="selectedValues" code="roles" renderAs="select" multiple />
|
* <g-sys-dict v-model="selectedValues" code="roles" renderAs="select" multiple />
|
||||||
*
|
*
|
||||||
* // 多选选择器(逗号模式)
|
* // 多选选择器(逗号模式)
|
||||||
* <g-sys-dict v-model="selectedValues" code="roles" renderAs="select" multiple multiple-model="comma" />
|
* <g-sys-dict v-model="selectedValues" code="roles" renderAs="select" multiple multiple-model="comma" />
|
||||||
*/
|
*/
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number, Boolean, Array, null] as PropType<string | number | boolean | any[] | Nullable>,
|
type: [String, Number, Boolean, Array, null] as PropType<string | number | boolean | any[] | Nullable>,
|
||||||
default: null,
|
default: null,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典编码,用于从字典中获取数据
|
* 字典编码,用于从字典中获取数据
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @required
|
* @required
|
||||||
* @example 'gender'
|
* @example 'gender'
|
||||||
*/
|
*/
|
||||||
code: {
|
code: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 直接传入的字典数据源(优先级高于code)
|
* 直接传入的字典数据源(优先级高于code)
|
||||||
* @type {DictItem[]}
|
* @type {DictItem[]}
|
||||||
* @example [{ label: '选项1', value: '1' }, { label: '选项2', value: '2' }]
|
* @example [{ label: '选项1', value: '1' }, { label: '选项2', value: '2' }]
|
||||||
*/
|
*/
|
||||||
data: {
|
data: {
|
||||||
type: Array as PropType<DictItem[]>,
|
type: Array as PropType<DictItem[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 是否为常量字典(true从常量列表获取,false从字典列表获取)
|
* 是否为常量字典(true从常量列表获取,false从字典列表获取)
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @default false
|
* @default false
|
||||||
* @example true
|
* @example true
|
||||||
*/
|
*/
|
||||||
isConst: {
|
isConst: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典项中用于显示的字段名
|
* 字典项中用于显示的字段名
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @default 'label'
|
* @default 'label'
|
||||||
* @example 'name'
|
* @example 'name'
|
||||||
*/
|
*/
|
||||||
propLabel: {
|
propLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'label',
|
default: 'label',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典项中用于取值的字段名
|
* 字典项中用于取值的字段名
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @default 'value'
|
* @default 'value'
|
||||||
* @example 'id'
|
* @example 'id'
|
||||||
*/
|
*/
|
||||||
propValue: {
|
propValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'value',
|
default: 'value',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典项过滤函数
|
* 字典项过滤函数
|
||||||
* @type {Function}
|
* @type {Function}
|
||||||
* @param {DictItem} dict - 当前字典项
|
* @param {DictItem} dict - 当前字典项
|
||||||
* @returns {boolean} - 是否保留该项
|
* @returns {boolean} - 是否保留该项
|
||||||
* @default (dict) => true
|
* @default (dict) => true
|
||||||
* @example
|
* @example
|
||||||
* // 只显示启用的字典项
|
* // 只显示启用的字典项
|
||||||
* :onItemFilter="(dict) => dict.status === 1"
|
* :onItemFilter="(dict) => dict.status === 1"
|
||||||
*/
|
*/
|
||||||
onItemFilter: {
|
onItemFilter: {
|
||||||
type: Function as PropType<(dict: DictItem) => boolean>,
|
type: Function as PropType<(dict: DictItem) => boolean>,
|
||||||
default: () => true,
|
default: () => true,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典项显示内容格式化函数
|
* 字典项显示内容格式化函数
|
||||||
* @type {Function}
|
* @type {Function}
|
||||||
* @param {DictItem} dict - 当前字典项
|
* @param {DictItem} dict - 当前字典项
|
||||||
* @returns {string|undefined|null} - 格式化后的显示内容
|
* @returns {string|undefined|null} - 格式化后的显示内容
|
||||||
* @default () => undefined
|
* @default () => undefined
|
||||||
* @example
|
* @example
|
||||||
* // 在标签前添加图标
|
* // 在标签前添加图标
|
||||||
* :onItemFormatter="(dict) => `${dict.label} <icon-user />`"
|
* :onItemFormatter="(dict) => `${dict.label} <icon-user />`"
|
||||||
*/
|
*/
|
||||||
onItemFormatter: {
|
onItemFormatter: {
|
||||||
type: Function as PropType<(dict: DictItem) => string | undefined | null>,
|
type: Function as PropType<(dict: DictItem) => string | undefined | null>,
|
||||||
default: () => undefined,
|
default: () => undefined,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 组件渲染方式
|
* 组件渲染方式
|
||||||
* @type {'tag'|'select'|'radio'|'checkbox'|'radio-button'}
|
* @type {'tag'|'select'|'radio'|'checkbox'|'radio-button'}
|
||||||
* @default 'tag'
|
* @default 'tag'
|
||||||
* @example 'select'
|
* @example 'select'
|
||||||
*/
|
*/
|
||||||
renderAs: {
|
renderAs: {
|
||||||
type: String as PropType<RenderType>,
|
type: String as PropType<RenderType>,
|
||||||
default: 'tag',
|
default: 'tag',
|
||||||
validator: isRenderType,
|
validator: isRenderType,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 是否多选(仅在renderAs为select/checkbox时有效)
|
* 是否多选(仅在renderAs为select/checkbox时有效)
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @default false
|
* @default false
|
||||||
* @example true
|
* @example true
|
||||||
*/
|
*/
|
||||||
multiple: {
|
multiple: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 多选值模式(仅在multiple为true时有效)
|
* 多选值模式(仅在multiple为true时有效)
|
||||||
* @type {'array'|'comma'}
|
* @type {'array'|'comma'}
|
||||||
* @default 'array'
|
* @default 'array'
|
||||||
* @example 'comma'
|
* @example 'comma'
|
||||||
*/
|
*/
|
||||||
multipleModel: {
|
multipleModel: {
|
||||||
type: String as PropType<MultipleModelType>,
|
type: String as PropType<MultipleModelType>,
|
||||||
default: MultipleModel.Array,
|
default: MultipleModel.Array,
|
||||||
validator: isMultipleModel,
|
validator: isMultipleModel,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 互斥配置项(仅在多选模式下有效)
|
* 互斥配置项(仅在多选模式下有效)
|
||||||
* @type {Array<MutexConfig>}
|
* @type {Array<MutexConfig>}
|
||||||
* @example
|
* @example
|
||||||
* :mutex-configs="[
|
* :mutex-configs="[
|
||||||
* { value: 'all', excludes: ['1', '2', '3'] },
|
* { value: 'all', excludes: ['1', '2', '3'] },
|
||||||
* { value: '1', excludes: ['all'] }
|
* { value: '1', excludes: ['all'] }
|
||||||
* ]"
|
* ]"
|
||||||
*/
|
*/
|
||||||
mutexConfigs: {
|
mutexConfigs: {
|
||||||
type: Array as PropType<MutexConfig[]>,
|
type: Array as PropType<MutexConfig[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件状态
|
||||||
|
* @property {DictItem[]} dictData - 原始字典数据
|
||||||
|
* @property {any} value - 当前值
|
||||||
|
*/
|
||||||
|
const state = reactive({
|
||||||
|
dictData: [] as DictItem[],
|
||||||
|
value: props.modelValue,
|
||||||
|
conversion: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -252,26 +263,26 @@ const props = defineProps({
|
|||||||
* @returns {DictItem[]} - 过滤并格式化后的字典数据
|
* @returns {DictItem[]} - 过滤并格式化后的字典数据
|
||||||
*/
|
*/
|
||||||
const formattedDictData = computed(() => {
|
const formattedDictData = computed(() => {
|
||||||
const baseData = state.dictData.filter(props.onItemFilter).map((item) => ({
|
const baseData = state.dictData.filter(props.onItemFilter).map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
label: item[props.propLabel],
|
label: item[props.propLabel],
|
||||||
value: item[props.propValue],
|
value: item[props.propValue],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 如果没有互斥配置或多选模式,直接返回基础数据
|
// 如果没有互斥配置或多选模式,直接返回基础数据
|
||||||
if (!props.multiple || !props.mutexConfigs || props.mutexConfigs.length === 0) {
|
if (!props.multiple || !props.mutexConfigs || props.mutexConfigs.length === 0) {
|
||||||
return baseData;
|
return baseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理互斥逻辑,设置禁用状态
|
// 处理互斥逻辑,设置禁用状态
|
||||||
return baseData.map((item) => {
|
return baseData.map((item) => {
|
||||||
// 检查当前项是否应该被禁用
|
// 检查当前项是否应该被禁用
|
||||||
const isDisabled = isItemDisabled(item.value, state.value, props.mutexConfigs);
|
const isDisabled = isItemDisabled(item.value, state.value, props.mutexConfigs);
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
disabled: isDisabled || item.disabled, // 保持原有的disabled状态
|
disabled: isDisabled || item.disabled, // 保持原有的disabled状态
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -280,15 +291,15 @@ const formattedDictData = computed(() => {
|
|||||||
* @returns {DictItem|DictItem[]|null} - 当前选中的字典项或字典项数组
|
* @returns {DictItem|DictItem[]|null} - 当前选中的字典项或字典项数组
|
||||||
*/
|
*/
|
||||||
const currentDictItems = computed(() => {
|
const currentDictItems = computed(() => {
|
||||||
if (!state.value) return null;
|
if (!state.value) return null;
|
||||||
|
|
||||||
if (Array.isArray(state.value)) {
|
if (Array.isArray(state.value)) {
|
||||||
// 去重
|
// 去重
|
||||||
const uniqueValues = [...new Set(state.value)];
|
const uniqueValues = [...new Set(state.value)];
|
||||||
return formattedDictData.value.filter((item) => uniqueValues.includes(item.value));
|
return formattedDictData.value.filter((item) => uniqueValues.includes(item.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return formattedDictData.value.find((item) => item.value == state.value) || null;
|
return formattedDictData.value.find((item) => item.value == state.value) || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -298,34 +309,34 @@ const currentDictItems = computed(() => {
|
|||||||
* @throws {Error} - 获取数据失败时抛出错误
|
* @throws {Error} - 获取数据失败时抛出错误
|
||||||
*/
|
*/
|
||||||
const getDataList = (): DictItem[] => {
|
const getDataList = (): DictItem[] => {
|
||||||
try {
|
try {
|
||||||
// 如果提供了 data 数据源,优先使用
|
// 如果提供了 data 数据源,优先使用
|
||||||
if (props.data && props.data.length > 0) {
|
if (props.data && props.data.length > 0) {
|
||||||
return props.data.map((item: any) => ({
|
return props.data.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
label: item[props.propLabel] ?? [item.name, item.desc].filter((x) => x).join('-'),
|
label: item[props.propLabel] ?? [item.name, item.desc].filter((x) => x).join('-'),
|
||||||
value: item[props.propValue] ?? item.code,
|
value: item[props.propValue] ?? item.code,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.code) {
|
if (!props.code) {
|
||||||
console.error('[g-sys-dict] code和data不能同时为空');
|
console.error('[g-sys-dict] code和data不能同时为空');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = props.isConst ? userStore.constList : userStore.dictList;
|
const source = props.isConst ? userStore.constList : userStore.dictList;
|
||||||
const data = props.isConst ? (source?.find((x: any) => x.code === props.code)?.data?.result ?? []) : (source[props.code] ?? []);
|
const data = props.isConst ? (source?.find((x: any) => x.code === props.code)?.data?.result ?? []) : (source[props.code] ?? []);
|
||||||
data.sort((a: number, b: number) => a - b);
|
data.sort((a: number, b: number) => a - b);
|
||||||
|
|
||||||
return data.map((item: any) => ({
|
return data.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
label: item[props.propLabel] ?? [item.name, item.desc].filter((x) => x).join('-'),
|
label: item[props.propLabel] ?? [item.name, item.desc].filter((x) => x).join('-'),
|
||||||
value: item[props.propValue] ?? item.code,
|
value: item[props.propValue] ?? item.code,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[g-sys-dict] 获取字典[${props.code}]数据失败:`, error);
|
console.error(`[g-sys-dict] 获取字典[${props.code}]数据失败:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -334,13 +345,13 @@ const getDataList = (): DictItem[] => {
|
|||||||
* @param {any} value - 待处理的值
|
* @param {any} value - 待处理的值
|
||||||
*/
|
*/
|
||||||
const processNumericValues = (value: any) => {
|
const processNumericValues = (value: any) => {
|
||||||
if (typeof value === 'number' || (Array.isArray(value) && typeof value[0] === 'number')) {
|
if (typeof value === 'number' || (Array.isArray(value) && typeof value[0] === 'number')) {
|
||||||
state.dictData.forEach((item) => {
|
state.dictData.forEach((item) => {
|
||||||
if (item.value) {
|
if (item.value) {
|
||||||
item.value = Number(item.value);
|
item.value = Number(item.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -350,36 +361,48 @@ const processNumericValues = (value: any) => {
|
|||||||
* @returns {any} - 解析后的值
|
* @returns {any} - 解析后的值
|
||||||
*/
|
*/
|
||||||
const parseMultipleValue = (value: any): any => {
|
const parseMultipleValue = (value: any): any => {
|
||||||
// 处理空值情况
|
// 处理空值情况
|
||||||
if (value === null || value === undefined || value === '') {
|
if (value === null || value === undefined || value === '') {
|
||||||
return props.multiple ? [] : value;
|
return props.multiple ? [] : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理字符串值
|
// 处理数字
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'number' && !state.conversion) {
|
||||||
const trimmedValue = value.trim();
|
try {
|
||||||
|
state.dictData.forEach((item) => {
|
||||||
|
if (item.value) item.value = Number(item.value);
|
||||||
|
});
|
||||||
|
state.conversion = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[g-sys-dict] 数字转换失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理JSON数组格式
|
// 处理字符串值
|
||||||
if (trimmedValue.startsWith('[') && trimmedValue.endsWith(']')) {
|
if (typeof value === 'string') {
|
||||||
try {
|
const trimmedValue = value.trim();
|
||||||
return JSON.parse(trimmedValue);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[g-sys-dict] 解析多选值失败:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理逗号分隔格式
|
// 处理JSON数组格式
|
||||||
if (props.multipleModel === MultipleModel.Comma && trimmedValue.includes(',')) {
|
if (trimmedValue.startsWith('[') && trimmedValue.endsWith(']')) {
|
||||||
return trimmedValue.split(',');
|
try {
|
||||||
}
|
return JSON.parse(trimmedValue);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[g-sys-dict] 解析多选值失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理单个值情况
|
// 处理逗号分隔格式
|
||||||
return props.multiple ? [trimmedValue] : trimmedValue;
|
if (props.multipleModel === MultipleModel.Comma && trimmedValue.includes(',')) {
|
||||||
}
|
return trimmedValue.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
// 其他情况直接返回
|
// 处理单个值情况
|
||||||
return value;
|
return props.multiple ? [trimmedValue] : trimmedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他情况直接返回
|
||||||
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -391,28 +414,28 @@ const parseMultipleValue = (value: any): any => {
|
|||||||
* @returns {boolean} - 是否应该禁用
|
* @returns {boolean} - 是否应该禁用
|
||||||
*/
|
*/
|
||||||
const isItemDisabled = (itemValue: string | number, currentValue: any, mutexConfigs: MutexConfig[]): boolean => {
|
const isItemDisabled = (itemValue: string | number, currentValue: any, mutexConfigs: MutexConfig[]): boolean => {
|
||||||
// 如果没有配置互斥规则,不禁用任何项
|
// 如果没有配置互斥规则,不禁用任何项
|
||||||
if (!mutexConfigs || mutexConfigs.length === 0) {
|
if (!mutexConfigs || mutexConfigs.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前选中的值数组
|
// 获取当前选中的值数组
|
||||||
const selectedValues = Array.isArray(currentValue) ? currentValue : currentValue ? [currentValue] : [];
|
const selectedValues = Array.isArray(currentValue) ? currentValue : currentValue ? [currentValue] : [];
|
||||||
|
|
||||||
// 检查每个互斥配置
|
// 检查每个互斥配置
|
||||||
for (const config of mutexConfigs) {
|
for (const config of mutexConfigs) {
|
||||||
// 如果互斥触发项已被选中,且当前项是被互斥项,则禁用
|
// 如果互斥触发项已被选中,且当前项是被互斥项,则禁用
|
||||||
if (selectedValues.includes(config.value) && config.excludes.includes(itemValue)) {
|
if (selectedValues.includes(config.value) && config.excludes.includes(itemValue)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果当前项是互斥触发项,且有被互斥项被选中,则禁用
|
// 如果当前项是互斥触发项,且有被互斥项被选中,则禁用
|
||||||
if (itemValue === config.value && config.excludes.some((exclude) => selectedValues.includes(exclude))) {
|
if (itemValue == config.value && config.excludes.some((exclude) => selectedValues.includes(exclude))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -423,20 +446,20 @@ const isItemDisabled = (itemValue: string | number, currentValue: any, mutexConf
|
|||||||
* @returns {any} - 处理后的值
|
* @returns {any} - 处理后的值
|
||||||
*/
|
*/
|
||||||
const handleMutex = (newValue: any, mutexConfigs: MutexConfig[]): any => {
|
const handleMutex = (newValue: any, mutexConfigs: MutexConfig[]): any => {
|
||||||
// 如果没有配置互斥规则,直接返回原值
|
// 如果没有配置互斥规则,直接返回原值
|
||||||
if (!mutexConfigs || mutexConfigs.length === 0) return newValue;
|
if (!mutexConfigs || mutexConfigs.length === 0) return newValue;
|
||||||
|
|
||||||
// 如果是单选模式,直接返回
|
// 如果是单选模式,直接返回
|
||||||
if (!props.multiple) return newValue;
|
if (!props.multiple) return newValue;
|
||||||
|
|
||||||
// 对于禁用模式,我们只需要确保新值是有效的(即没有违反互斥规则)
|
// 对于禁用模式,我们只需要确保新值是有效的(即没有违反互斥规则)
|
||||||
// 实际的禁用逻辑在formattedDictData中处理
|
// 实际的禁用逻辑在formattedDictData中处理
|
||||||
let resultValue = Array.isArray(newValue) ? [...newValue] : newValue ? [newValue] : [];
|
let resultValue = Array.isArray(newValue) ? [...newValue] : newValue ? [newValue] : [];
|
||||||
|
|
||||||
// 过滤掉无效的值(可能由于异步更新导致的无效选择)
|
// 过滤掉无效的值(可能由于异步更新导致的无效选择)
|
||||||
const validValues = formattedDictData.value.filter((item) => !item.disabled).map((item) => item.value);
|
const validValues = formattedDictData.value.filter((item) => !item.disabled).map((item) => item.value);
|
||||||
|
|
||||||
return resultValue.filter((val) => validValues.includes(val));
|
return resultValue.filter((val) => validValues.includes(val));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -445,30 +468,30 @@ const handleMutex = (newValue: any, mutexConfigs: MutexConfig[]): any => {
|
|||||||
* @param {any} newValue - 新值
|
* @param {any} newValue - 新值
|
||||||
*/
|
*/
|
||||||
const updateValue = (newValue: any) => {
|
const updateValue = (newValue: any) => {
|
||||||
// 如果有互斥配置,先处理互斥
|
// 如果有互斥配置,先处理互斥
|
||||||
let processedValue = newValue;
|
let processedValue = newValue;
|
||||||
if (props.mutexConfigs && props.mutexConfigs.length > 0) {
|
if (props.mutexConfigs && props.mutexConfigs.length > 0) {
|
||||||
processedValue = handleMutex(newValue, props.mutexConfigs);
|
processedValue = handleMutex(newValue, props.mutexConfigs);
|
||||||
}
|
}
|
||||||
|
|
||||||
let emitValue = processedValue;
|
let emitValue = processedValue;
|
||||||
if (props.multipleModel === MultipleModel.Comma) {
|
if (props.multipleModel === MultipleModel.Comma) {
|
||||||
if (Array.isArray(processedValue)) {
|
if (Array.isArray(processedValue)) {
|
||||||
emitValue = processedValue.length > 0 ? processedValue.sort().join(',') : undefined;
|
emitValue = processedValue.length > 0 ? processedValue.sort().join(',') : [];
|
||||||
} else if (processedValue === null || processedValue === undefined) {
|
} else if (processedValue === null || processedValue === undefined) {
|
||||||
emitValue = undefined;
|
emitValue = undefined;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Array.isArray(processedValue)) {
|
if (Array.isArray(processedValue)) {
|
||||||
emitValue = processedValue.length > 0 ? processedValue.sort() : undefined;
|
emitValue = processedValue.length > 0 ? processedValue.sort() : [];
|
||||||
} else if (processedValue === null || processedValue === undefined) {
|
} else if (processedValue === null || processedValue === undefined) {
|
||||||
emitValue = undefined;
|
emitValue = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.value = processedValue;
|
state.value = processedValue;
|
||||||
emit('update:modelValue', emitValue === '' ? undefined : emitValue);
|
emit('update:modelValue', emitValue === '' || emitValue.length === 0 ? undefined : emitValue);
|
||||||
emit('change', state.value, currentDictItems, state.dictData);
|
emit('change', state.value, currentDictItems, state.dictData);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -478,7 +501,7 @@ const updateValue = (newValue: any) => {
|
|||||||
* @returns {TagType} - 合法的标签类型
|
* @returns {TagType} - 合法的标签类型
|
||||||
*/
|
*/
|
||||||
const ensureTagType = (item: DictItem): TagType => {
|
const ensureTagType = (item: DictItem): TagType => {
|
||||||
return TAG_TYPES.includes(item.tagType as TagType) ? (item.tagType as TagType) : 'primary';
|
return TAG_TYPES.includes(item.tagType as TagType) ? (item.tagType as TagType) : 'primary';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -488,9 +511,9 @@ const ensureTagType = (item: DictItem): TagType => {
|
|||||||
* @returns {string} - 显示文本
|
* @returns {string} - 显示文本
|
||||||
*/
|
*/
|
||||||
const getDisplayText = (dict?: DictItem): string => {
|
const getDisplayText = (dict?: DictItem): string => {
|
||||||
if (!dict) return String(state.value || '');
|
if (!dict) return String(state.value || '');
|
||||||
const formattedText = props.onItemFormatter?.(dict);
|
const formattedText = props.onItemFormatter?.(dict);
|
||||||
return formattedText ?? dict[props.propLabel] ?? '';
|
return formattedText ?? dict[props.propLabel] ?? '';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -498,20 +521,20 @@ const getDisplayText = (dict?: DictItem): string => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const initData = () => {
|
const initData = () => {
|
||||||
// 验证 code 和 data 不能同时为空
|
// 验证 code 和 data 不能同时为空
|
||||||
if (!props.code && (!props.data || props.data.length === 0)) {
|
if (!props.code && (!props.data || props.data.length === 0)) {
|
||||||
console.error('[g-sys-dict] code和data不能同时为空');
|
console.error('[g-sys-dict] code和data不能同时为空');
|
||||||
state.dictData = [];
|
state.dictData = [];
|
||||||
state.value = props.multiple ? [] : null;
|
state.value = props.multiple ? [] : null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.dictData = getDataList();
|
state.dictData = getDataList();
|
||||||
processNumericValues(props.modelValue);
|
processNumericValues(props.modelValue);
|
||||||
const initialValue = parseMultipleValue(props.modelValue);
|
const initialValue = parseMultipleValue(props.modelValue);
|
||||||
if (initialValue !== state.value) {
|
if (initialValue !== state.value) {
|
||||||
state.value = initialValue;
|
state.value = initialValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -519,88 +542,78 @@ const initData = () => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const validateInitialValue = () => {
|
const validateInitialValue = () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (props.renderAs === 'tag' || !state.value) return resolve(undefined);
|
if (props.renderAs === 'tag' || !state.value) return resolve(undefined);
|
||||||
if (Array.isArray(state.value)) {
|
if (Array.isArray(state.value)) {
|
||||||
const errorValues = state.value.filter((val) => state.dictData.find((e) => e[props.propValue] == val) === undefined);
|
const errorValues = state.value.filter((val) => state.dictData.find((e) => e[props.propValue] == val) === undefined);
|
||||||
if (errorValues && errorValues.length > 0) {
|
if (errorValues && errorValues.length > 0) {
|
||||||
reject(`[g-sys-dict] 未匹配到选项值:${JSON.stringify(errorValues)}`);
|
reject(`[g-sys-dict] 未匹配到选项值:${JSON.stringify(errorValues)}`);
|
||||||
}
|
}
|
||||||
} else if (state.value) {
|
} else if (state.value) {
|
||||||
if (!state.dictData.find((e) => e[props.propValue] === state.value)) {
|
if (!state.dictData.find((e) => e[props.propValue] == state.value)) {
|
||||||
reject(`[g-sys-dict] 未匹配到选项值:${state.value}`);
|
reject(`[g-sys-dict] 未匹配到选项值:${state.value}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve(undefined);
|
resolve(undefined);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件状态
|
|
||||||
* @property {DictItem[]} dictData - 原始字典数据
|
|
||||||
* @property {any} value - 当前值
|
|
||||||
*/
|
|
||||||
const state = reactive({
|
|
||||||
dictData: [] as DictItem[],
|
|
||||||
value: parseMultipleValue(props.modelValue),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听数据变化
|
// 监听数据变化
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
state.value = parseMultipleValue(newValue);
|
state.value = parseMultipleValue(newValue);
|
||||||
validateInitialValue();
|
validateInitialValue();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
watch(() => [userStore.dictList, userStore.constList, props.data, state], initData, { immediate: true });
|
watch(() => [userStore.dictList, userStore.constList, props.data, state], initData, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 渲染标签 -->
|
<!-- 渲染标签 -->
|
||||||
<template v-if="props.renderAs === 'tag'">
|
<template v-if="props.renderAs === 'tag'">
|
||||||
<template v-if="Array.isArray(currentDictItems)">
|
<template v-if="Array.isArray(currentDictItems)">
|
||||||
<el-tag v-for="(item, index) in currentDictItems" :key="index" v-bind="$attrs" :type="ensureTagType(item)" :style="item.styleSetting" :class="item.classSetting" class="mr2">
|
<el-tag v-for="(item, index) in currentDictItems" :key="index" v-bind="$attrs" :type="ensureTagType(item)" :style="item.styleSetting" :class="item.classSetting" class="mr2">
|
||||||
{{ getDisplayText(item) }}
|
{{ getDisplayText(item) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-tag v-if="currentDictItems" v-bind="$attrs" :type="ensureTagType(currentDictItems)" :style="currentDictItems.styleSetting" :class="currentDictItems.classSetting">
|
<el-tag v-if="currentDictItems" v-bind="$attrs" :type="ensureTagType(currentDictItems)" :style="currentDictItems.styleSetting" :class="currentDictItems.classSetting">
|
||||||
{{ getDisplayText(currentDictItems) }}
|
{{ getDisplayText(currentDictItems) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span v-else>{{ getDisplayText() }}</span>
|
<span v-else>{{ getDisplayText() }}</span>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 渲染选择器 -->
|
<!-- 渲染选择器 -->
|
||||||
<el-select v-else-if="props.renderAs === 'select'" v-model="state.value" v-bind="$attrs" :multiple="props.multiple" @change="updateValue" filterable allow-create default-first-option clearable>
|
<el-select v-else-if="props.renderAs === 'select'" v-model="state.value" v-bind="$attrs" :multiple="props.multiple" @change="updateValue" filterable allow-create default-first-option clearable>
|
||||||
<el-option v-for="(item, index) in formattedDictData" :key="index" :label="getDisplayText(item)" :value="item.value" :disabled="item.disabled" />
|
<el-option v-for="(item, index) in formattedDictData" :key="index" :label="getDisplayText(item)" :value="item.value" :disabled="item.disabled" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<!-- 多选框(多选) -->
|
<!-- 多选框(多选) -->
|
||||||
<el-checkbox-group v-else-if="props.renderAs === 'checkbox'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
<el-checkbox-group v-else-if="props.renderAs === 'checkbox'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
||||||
<el-checkbox v-for="(item, index) in formattedDictData" :key="index" :value="item.value" :label="getDisplayText(item)" :disabled="item.disabled" />
|
<el-checkbox v-for="(item, index) in formattedDictData" :key="index" :value="item.value" :label="getDisplayText(item)" :disabled="item.disabled" />
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
|
|
||||||
<!-- 多选框-按钮(多选) -->
|
<!-- 多选框-按钮(多选) -->
|
||||||
<el-checkbox-group v-else-if="props.renderAs === 'checkbox-button'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
<el-checkbox-group v-else-if="props.renderAs === 'checkbox-button'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
||||||
<el-checkbox-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value" :disabled="item.disabled">
|
<el-checkbox-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value" :disabled="item.disabled">
|
||||||
{{ getDisplayText(item) }}
|
{{ getDisplayText(item) }}
|
||||||
</el-checkbox-button>
|
</el-checkbox-button>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
|
|
||||||
<!-- 渲染单选框 -->
|
<!-- 渲染单选框 -->
|
||||||
<el-radio-group v-else-if="props.renderAs === 'radio'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
<el-radio-group v-else-if="props.renderAs === 'radio'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
||||||
<el-radio v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
|
<el-radio v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
|
||||||
{{ getDisplayText(item) }}
|
{{ getDisplayText(item) }}
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
|
|
||||||
<!-- 渲染单选框按钮 -->
|
<!-- 渲染单选框按钮 -->
|
||||||
<el-radio-group v-else-if="props.renderAs === 'radio-button'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
<el-radio-group v-else-if="props.renderAs === 'radio-button'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
||||||
<el-radio-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
|
<el-radio-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
|
||||||
{{ getDisplayText(item) }}
|
{{ getDisplayText(item) }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
Loading…
Reference in New Issue
Block a user