Merge pull request ' fix(sysDict): 修复字典值存在空值时异常的问题' (#408) from jasondom/Admin.NET.Pro:v2-1 into v2

Reviewed-on: https://code.adminnet.top/Admin.NET/Admin.NET.Pro/pulls/408
This commit is contained in:
zuohuaijun 2025-08-19 12:43:40 +08:00
commit 72e699bbc0
4 changed files with 45 additions and 5812 deletions

2
Web/.gitignore vendored
View File

@ -25,3 +25,5 @@ pnpm-debug.log*
pnpm-lock.yaml
package-lock.json
/upload
/lang/index.js
/lang/index.json

View File

@ -1,87 +0,0 @@
// 导入国际化JSON文件
import langJSON from './index.json'
(function () {
// 定义翻译函数
let $tr = function (key, val, nameSpace) {
// 获取指定命名空间下的语言包
const langPackage = $tr[nameSpace];
// 返回翻译结果,如果不存在则返回默认值
return (langPackage || {})[key] || val;
};
// 定义简单翻译函数,直接返回传入的值
let $$tr = function (val) {
return val;
};
globalThis.$deepScan = function (val) {
return val;
};
globalThis.$iS = function (val, args) {
// 如果参数不是字符串或数组,直接返回原值
if (typeof val !== 'string' || !Array.isArray(args)) {
return val;
}
try {
// 使用更安全的正则表达式替换方式
return val.replace(/\$(?:\{|\)(\d+)(?:\}|\)/g, (match, index) => {
// 将index转换为数字
const position = parseInt(index, 10);
// 如果args[position]存在则替换,否则保留原占位符
return args[position] !== undefined ? String(args[position]) : match;
});
} catch (error) {
console.warn('字符串替换过程出现异常:', error);
return val;
}
}
// 定义设置语言包的方法
$tr.locale = function (locale, nameSpace) {
// 将指定命名空间下的语言包设置为传入的locale
$tr[nameSpace] = locale || {};
};
// 将翻译函数挂载到globalThis对象上如果已经存在则使用已有的
globalThis.$tr = globalThis.$tr || $tr;
// 将简单翻译函数挂载到globalThis对象上
globalThis.$$tr = $$tr;
// 定义从JSON文件中获取指定键的语言对象的方法
globalThis._getJSONKey = function (key, insertJSONObj = undefined) {
// 获取JSON对象
const JSONObj = insertJSONObj;
// 初始化语言对象
const langObj = {};
// 遍历JSON对象的所有键
Object.keys(JSONObj).forEach((value) => {
// 将每个语言的对应键值添加到语言对象中
langObj[value] = JSONObj[value][key];
});
// 返回语言对象
return langObj;
};
})();
// 定义语言映射对象
const langMap = {
'en': (globalThis && globalThis.lang && globalThis.lang.en) ? globalThis.lang.en : globalThis._getJSONKey('en', langJSON),
'zhcn': (globalThis && globalThis.lang && globalThis.lang.zhcn) ? globalThis.lang.zhcn : globalThis._getJSONKey('zh-cn', langJSON)
};
globalThis.langMap = langMap;
// 存储语言是否存在
// 判断 globalThis.localStorage.getItem 是否为函数
const isFunction = (fn) => {
return typeof fn === 'function';
};
const withStorageLang = isFunction && globalThis && globalThis.localStorage &&
isFunction(globalThis.localStorage.getItem) && globalThis.localStorage.getItem('lang');
const withStorageCommonLang = isFunction && globalThis && globalThis.localStorage &&
isFunction(globalThis.localStorage.getItem) && globalThis.localStorage.getItem('');
// 从本地存储中获取通用语言,如果不存在则使用空字符串
const commonLang = withStorageCommonLang ? globalThis.localStorage.getItem('') : '';
// 从本地存储中获取当前语言,如果不存在则使用源语言
const baseLang = withStorageLang ? globalThis.localStorage.getItem('lang') : 'zhcn';
const lang = commonLang ? commonLang : baseLang;
// 根据当前语言设置翻译函数的语言包
globalThis.$tr.locale(globalThis.langMap[lang], 'lang');
globalThis.$changeLang = (lang) => {
globalThis.$tr.locale(globalThis.langMap[lang], 'lang');
};

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
* @constant
*/
const RENDER_TYPES = ['tag', 'select', 'radio', 'checkbox', 'radio-button'] as const;
type RenderType = (typeof RENDER_TYPES)[number];
type RenderType = typeof RENDER_TYPES[number];
/**
* 支持的标签类型常量
@ -24,7 +24,7 @@ type RenderType = (typeof RENDER_TYPES)[number];
* @constant
*/
const TAG_TYPES = ['success', 'warning', 'info', 'primary', 'danger'] as const;
type TagType = (typeof TAG_TYPES)[number];
type TagType = typeof TAG_TYPES[number];
/**
* 字典项数据结构
@ -40,22 +40,10 @@ 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}
@ -93,7 +81,11 @@ import { reactive, watch, computed, PropType } from 'vue';
import { useUserInfo } from '/@/stores/userInfo';
const userStore = useUserInfo();
const emit = defineEmits(['update:modelValue', 'change']);
const emit = defineEmits(['update:modelValue']);
const isEmpty = (value: any) => {
return value == null || value === '' || value === undefined;
};
/**
* 组件属性定义
@ -218,19 +210,6 @@ 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: () => [],
},
});
/**
@ -239,26 +218,13 @@ const props = defineProps({
* @returns {DictItem[]} - 过滤并格式化后的字典数据
*/
const formattedDictData = computed(() => {
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
};
});
return state.dictData
.filter(props.onItemFilter)
.map(item => ({
...item,
label: item[props.propLabel],
value: item[props.propValue],
}));
});
/**
@ -267,15 +233,17 @@ const formattedDictData = computed(() => {
* @returns {DictItem|DictItem[]|null} - 当前选中的字典项或字典项数组
*/
const currentDictItems = computed(() => {
if (!state.value) return null;
if (isEmpty(state.value)) return null;
if (Array.isArray(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;
});
/**
@ -286,13 +254,15 @@ const currentDictItems = computed(() => {
*/
const getDataList = (): DictItem[] => {
try {
if (!props.code) {
if (isEmpty(props.code)) {
console.error('字典编码不能为空');
return [];
}
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] ?? [];
return data.map((item: any) => ({
...item,
@ -312,8 +282,8 @@ const getDataList = (): DictItem[] => {
*/
const processNumericValues = (value: any) => {
if (typeof value === 'number' || (Array.isArray(value) && typeof value[0] === 'number')) {
state.dictData.forEach((item) => {
if (item.value) {
state.dictData.forEach(item => {
if (!isEmpty(item.value)) {
item.value = Number(item.value);
}
});
@ -328,7 +298,7 @@ const processNumericValues = (value: any) => {
*/
const parseMultipleValue = (value: any): any => {
//
if (value === null || value === undefined || value === '') {
if (isEmpty(value)) {
return props.multiple ? [] : value;
}
@ -354,14 +324,11 @@ const parseMultipleValue = (value: any): any => {
//
if (props.multipleModel === MultipleModel.Comma && trimmedValue.includes(',')) {
//
return [
...new Set(
trimmedValue
.split(',')
.map((item) => item.trim())
.filter(Boolean)
),
];
return [...new Set(
trimmedValue.split(',')
.map(item => item.trim())
.filter(Boolean)
)];
}
//
@ -377,83 +344,16 @@ const parseMultipleValue = (value: any): any => {
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 (props.mutexConfigs && props.mutexConfigs.length > 0) {
processedValue = handleMutex(newValue, props.mutexConfigs);
// console.log('[g-sys-dict] :', state.dictData);
}
//
if (Array.isArray(processedValue)) {
processedValue = [...new Set(processedValue.filter((v) => v !== null && v !== undefined && v !== ''))];
if (Array.isArray(newValue)) {
processedValue = [...new Set(newValue.filter(v => v !== null && v !== undefined && v !== ''))];
}
let emitValue = processedValue;
@ -461,14 +361,13 @@ const updateValue = (newValue: any) => {
if (props.multipleModel === MultipleModel.Comma) {
if (Array.isArray(processedValue)) {
emitValue = processedValue.length > 0 ? processedValue.join(',') : '';
} else if (processedValue === null || processedValue === undefined) {
} else if (isEmpty(processedValue)) {
emitValue = '';
}
}
state.value = processedValue;
emit('update:modelValue', emitValue);
emit('change', state.value, state.dictData);
};
/**
@ -478,7 +377,7 @@ const updateValue = (newValue: any) => {
* @returns {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';
};
/**
@ -517,14 +416,11 @@ const state = reactive({
});
//
watch(
() => props.modelValue,
(newValue) => {
state.value = parseMultipleValue(newValue);
}
);
watch(() => props.modelValue, (newValue) => {
state.value = parseMultipleValue(newValue);
});
watch(() => [userStore.dictList, userStore.constList, state], initData, { immediate: true });
watch(() => [userStore.dictList, userStore.constList], initData, { immediate: true });
</script>
<template>
@ -545,12 +441,12 @@ watch(() => [userStore.dictList, userStore.constList, state], initData, { immedi
<!-- 渲染选择器 -->
<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-option v-for="(item, index) in formattedDictData" :key="index" :label="getDisplayText(item)" :value="item.value" />
</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" :disabled="item.disabled">
<el-checkbox-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
{{ getDisplayText(item) }}
</el-checkbox-button>
</el-checkbox-group>
@ -569,4 +465,4 @@ watch(() => [userStore.dictList, userStore.constList, state], initData, { immedi
</el-radio-button>
</el-radio-group>
</template>
<style scoped lang="scss"></style>
<style scoped lang="scss"></style>