😎优化字典组件、下拉组件

This commit is contained in:
zuohuaijun 2025-09-14 11:38:38 +08:00
parent 0bac43bc87
commit 41b8b361cd
3 changed files with 420 additions and 380 deletions

View File

@ -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 }
); );

View File

@ -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>

View File

@ -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>