🍊 feat(sysDict): 新增checkbox-button渲染模式
- 绑定值为空时,将值设置为`undefined`, 避免空字符串、空数组 - 多选值排序后再更新绑定 - 优化无意义的去重逻辑
This commit is contained in:
parent
91bf37c03d
commit
5d61590ad1
@ -15,7 +15,7 @@
|
||||
* @type {Array}
|
||||
* @constant
|
||||
*/
|
||||
const RENDER_TYPES = ['tag', 'select', 'radio', 'checkbox', 'radio-button'] as const;
|
||||
const RENDER_TYPES = ['tag', 'select', 'radio', 'checkbox', 'checkbox-button', 'radio-button'] as const;
|
||||
type RenderType = (typeof RENDER_TYPES)[number];
|
||||
|
||||
/**
|
||||
@ -40,10 +40,22 @@ interface DictItem {
|
||||
tagType?: TagType;
|
||||
styleSetting?: string;
|
||||
classSetting?: string;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
value?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 互斥选项配置
|
||||
* @type {Object}
|
||||
* @property {string|number} value - 触发互斥的选项值
|
||||
* @property {Array<string|number>} excludes - 被互斥的选项值列表
|
||||
*/
|
||||
interface MutexConfig {
|
||||
value: string | number;
|
||||
excludes: (string | number)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 多选值模式枚举
|
||||
* @enum {string}
|
||||
@ -81,11 +93,7 @@ import { reactive, watch, computed, PropType } from 'vue';
|
||||
import { useUserInfo } from '/@/stores/userInfo';
|
||||
|
||||
const userStore = useUserInfo();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const isEmpty = (value: any) => {
|
||||
return value == null || value === '' || value === undefined;
|
||||
};
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
/**
|
||||
* 组件属性定义
|
||||
@ -210,6 +218,19 @@ const props = defineProps({
|
||||
default: MultipleModel.Array,
|
||||
validator: isMultipleModel,
|
||||
},
|
||||
/**
|
||||
* 互斥配置项(仅在多选模式下有效)
|
||||
* @type {Array<MutexConfig>}
|
||||
* @example
|
||||
* :mutex-configs="[
|
||||
* { value: 'all', excludes: ['1', '2', '3'] },
|
||||
* { value: '1', excludes: ['all'] }
|
||||
* ]"
|
||||
*/
|
||||
mutexConfigs: {
|
||||
type: Array as PropType<MutexConfig[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@ -218,11 +239,26 @@ const props = defineProps({
|
||||
* @returns {DictItem[]} - 过滤并格式化后的字典数据
|
||||
*/
|
||||
const formattedDictData = computed(() => {
|
||||
return state.dictData.filter(props.onItemFilter).map((item) => ({
|
||||
const baseData = state.dictData.filter(props.onItemFilter).map((item) => ({
|
||||
...item,
|
||||
label: item[props.propLabel],
|
||||
value: item[props.propValue],
|
||||
}));
|
||||
|
||||
// 如果没有互斥配置或多选模式,直接返回基础数据
|
||||
if (!props.multiple || !props.mutexConfigs || props.mutexConfigs.length === 0) {
|
||||
return baseData;
|
||||
}
|
||||
|
||||
// 处理互斥逻辑,设置禁用状态
|
||||
return baseData.map((item) => {
|
||||
// 检查当前项是否应该被禁用
|
||||
const isDisabled = isItemDisabled(item.value, state.value, props.mutexConfigs);
|
||||
return {
|
||||
...item,
|
||||
disabled: isDisabled || item.disabled, // 保持原有的disabled状态
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@ -231,7 +267,7 @@ const formattedDictData = computed(() => {
|
||||
* @returns {DictItem|DictItem[]|null} - 当前选中的字典项或字典项数组
|
||||
*/
|
||||
const currentDictItems = computed(() => {
|
||||
if (isEmpty(state.value)) return null;
|
||||
if (!state.value) return null;
|
||||
|
||||
if (Array.isArray(state.value)) {
|
||||
// 确保回显时也去重
|
||||
@ -250,8 +286,8 @@ const currentDictItems = computed(() => {
|
||||
*/
|
||||
const getDataList = (): DictItem[] => {
|
||||
try {
|
||||
if (isEmpty(props.code)) {
|
||||
console.error('字典编码不能为空');
|
||||
if (!props.code) {
|
||||
console.error('[g-sys-dict] 字典编码不能为空');
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -264,7 +300,7 @@ const getDataList = (): DictItem[] => {
|
||||
value: item[props.propValue] ?? item.code,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`获取字典[${props.code}]数据失败:`, error);
|
||||
console.error(`[g-sys-dict] 获取字典[${props.code}]数据失败:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@ -277,7 +313,7 @@ const getDataList = (): DictItem[] => {
|
||||
const processNumericValues = (value: any) => {
|
||||
if (typeof value === 'number' || (Array.isArray(value) && typeof value[0] === 'number')) {
|
||||
state.dictData.forEach((item) => {
|
||||
if (!isEmpty(item.value)) {
|
||||
if (item.value) {
|
||||
item.value = Number(item.value);
|
||||
}
|
||||
});
|
||||
@ -292,7 +328,7 @@ const processNumericValues = (value: any) => {
|
||||
*/
|
||||
const parseMultipleValue = (value: any): any => {
|
||||
// 处理空值情况
|
||||
if (isEmpty(value)) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return props.multiple ? [] : value;
|
||||
}
|
||||
|
||||
@ -303,12 +339,7 @@ const parseMultipleValue = (value: any): any => {
|
||||
// 处理JSON数组格式
|
||||
if (trimmedValue.startsWith('[') && trimmedValue.endsWith(']')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmedValue);
|
||||
if (Array.isArray(parsed)) {
|
||||
// 去重处理
|
||||
return [...new Set(parsed.filter(Boolean))];
|
||||
}
|
||||
return parsed;
|
||||
return JSON.parse(trimmedValue);
|
||||
} catch (error) {
|
||||
console.warn('[g-sys-dict] 解析多选值失败:', error);
|
||||
return [];
|
||||
@ -317,54 +348,104 @@ const parseMultipleValue = (value: any): any => {
|
||||
|
||||
// 处理逗号分隔格式
|
||||
if (props.multipleModel === MultipleModel.Comma && trimmedValue.includes(',')) {
|
||||
// 去重处理
|
||||
return [
|
||||
...new Set(
|
||||
trimmedValue
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
),
|
||||
];
|
||||
return trimmedValue.split(',');
|
||||
}
|
||||
|
||||
// 处理单个值情况
|
||||
return props.multiple ? [trimmedValue] : trimmedValue;
|
||||
}
|
||||
|
||||
// 处理数组值 - 添加去重
|
||||
if (Array.isArray(value)) {
|
||||
return [...new Set(value.filter(Boolean))];
|
||||
}
|
||||
|
||||
// 其他情况直接返回
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查选项是否应该被禁用
|
||||
* @function
|
||||
* @param {string|number} itemValue - 当前选项的值
|
||||
* @param {any} currentValue - 当前选中的值
|
||||
* @param {MutexConfig[]} mutexConfigs - 互斥配置列表
|
||||
* @returns {boolean} - 是否应该禁用
|
||||
*/
|
||||
const isItemDisabled = (itemValue: string | number, currentValue: any, mutexConfigs: MutexConfig[]): boolean => {
|
||||
// 如果没有配置互斥规则,不禁用任何项
|
||||
if (!mutexConfigs || mutexConfigs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取当前选中的值数组
|
||||
const selectedValues = Array.isArray(currentValue) ? currentValue : currentValue ? [currentValue] : [];
|
||||
|
||||
// 检查每个互斥配置
|
||||
for (const config of mutexConfigs) {
|
||||
// 如果互斥触发项已被选中,且当前项是被互斥项,则禁用
|
||||
if (selectedValues.includes(config.value) && config.excludes.includes(itemValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果当前项是互斥触发项,且有被互斥项被选中,则禁用
|
||||
if (itemValue === config.value && config.excludes.some((exclude) => selectedValues.includes(exclude))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 互斥处理函数
|
||||
* @function
|
||||
* @param {any} newValue - 新选中的值
|
||||
* @param {MutexConfig[]} mutexConfigs - 互斥配置列表
|
||||
* @returns {any} - 处理后的值
|
||||
*/
|
||||
const handleMutex = (newValue: any, mutexConfigs: MutexConfig[]): any => {
|
||||
// 如果没有配置互斥规则,直接返回原值
|
||||
if (!mutexConfigs || mutexConfigs.length === 0) return newValue;
|
||||
|
||||
// 如果是单选模式,直接返回
|
||||
if (!props.multiple) return newValue;
|
||||
|
||||
// 对于禁用模式,我们只需要确保新值是有效的(即没有违反互斥规则)
|
||||
// 实际的禁用逻辑在formattedDictData中处理
|
||||
let resultValue = Array.isArray(newValue) ? [...newValue] : newValue ? [newValue] : [];
|
||||
|
||||
// 过滤掉无效的值(可能由于异步更新导致的无效选择)
|
||||
const validValues = formattedDictData.value.filter((item) => !item.disabled).map((item) => item.value);
|
||||
|
||||
return resultValue.filter((val) => validValues.includes(val));
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新绑定值(修复逗号模式问题)
|
||||
* @function
|
||||
* @param {any} newValue - 新值
|
||||
*/
|
||||
const updateValue = (newValue: any) => {
|
||||
// 先对值进行去重处理
|
||||
// 如果有互斥配置,先处理互斥
|
||||
let processedValue = newValue;
|
||||
if (Array.isArray(newValue)) {
|
||||
processedValue = [...new Set(newValue.filter((v) => v !== null && v !== undefined && v !== ''))];
|
||||
if (props.mutexConfigs && props.mutexConfigs.length > 0) {
|
||||
processedValue = handleMutex(newValue, props.mutexConfigs);
|
||||
}
|
||||
|
||||
let emitValue = processedValue;
|
||||
|
||||
if (props.multipleModel === MultipleModel.Comma) {
|
||||
if (Array.isArray(processedValue)) {
|
||||
emitValue = processedValue.length > 0 ? processedValue.join(',') : '';
|
||||
} else if (isEmpty(processedValue)) {
|
||||
emitValue = '';
|
||||
emitValue = processedValue.length > 0 ? processedValue.sort().join(',') : undefined;
|
||||
} else if (processedValue === null || processedValue === undefined) {
|
||||
emitValue = undefined;
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(processedValue)) {
|
||||
emitValue = processedValue.length > 0 ? processedValue.sort() : undefined;
|
||||
} else if (processedValue === null || processedValue === undefined) {
|
||||
emitValue = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
state.value = processedValue;
|
||||
emit('update:modelValue', emitValue);
|
||||
emit('update:modelValue', emitValue === '' ? undefined : emitValue);
|
||||
emit('change', state.value, state.dictData);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -402,6 +483,27 @@ const initData = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 校验初始值对应的选项是否存在
|
||||
* @function
|
||||
*/
|
||||
const validateInitialValue = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (props.renderAs === 'tag' || !state.value) return resolve(undefined);
|
||||
if (Array.isArray(state.value)) {
|
||||
const errorValues = state.value.filter(val => state.dictData.find(e => e[props.propValue] == val) === undefined);
|
||||
if (errorValues && errorValues.length > 0) {
|
||||
reject(`[g-sys-dict] 未匹配到选项值:${JSON.stringify(errorValues)}`);
|
||||
}
|
||||
} else if (state.value) {
|
||||
if (!state.dictData.find(e => e[props.propValue] === state.value)) {
|
||||
reject(`[g-sys-dict] 未匹配到选项值:${state.value}`);
|
||||
}
|
||||
}
|
||||
resolve(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件状态
|
||||
* @property {DictItem[]} dictData - 原始字典数据
|
||||
@ -413,14 +515,11 @@ const state = reactive({
|
||||
});
|
||||
|
||||
// 监听数据变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
state.value = parseMultipleValue(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
watch(() => [userStore.dictList, userStore.constList], initData, { immediate: true });
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
state.value = parseMultipleValue(newValue);
|
||||
validateInitialValue();
|
||||
});
|
||||
watch(() => [userStore.dictList, userStore.constList, state], initData, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -440,13 +539,18 @@ watch(() => [userStore.dictList, userStore.constList], initData, { immediate: tr
|
||||
</template>
|
||||
|
||||
<!-- 渲染选择器 -->
|
||||
<el-select v-else-if="props.renderAs === 'select'" v-model="state.value" v-bind="$attrs" :multiple="props.multiple" @change="updateValue" filterable clearable>
|
||||
<el-option v-for="(item, index) in formattedDictData" :key="index" :label="getDisplayText(item)" :value="item.value" />
|
||||
<el-select v-else-if="props.renderAs === 'select'" v-model="state.value" v-bind="$attrs" :multiple="props.multiple" @change="updateValue" clearable>
|
||||
<el-option v-for="(item, index) in formattedDictData" :key="index" :label="getDisplayText(item)" :value="item.value" :disabled="item.disabled" />
|
||||
</el-select>
|
||||
|
||||
<!-- 渲染复选框(多选) -->
|
||||
<!-- 多选框(多选) -->
|
||||
<el-checkbox-group v-else-if="props.renderAs === 'checkbox'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
||||
<el-checkbox-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
|
||||
<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 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">
|
||||
{{ getDisplayText(item) }}
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
@ -465,4 +569,4 @@ watch(() => [userStore.dictList, userStore.constList], initData, { immediate: tr
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss"></style>
|
||||
Loading…
Reference in New Issue
Block a user