😎1、优化字典值合规性校验特性 2、优化种子初始化 3、优化前端导入数据
This commit is contained in:
parent
cdb58a28ab
commit
d32a4b2613
@ -50,23 +50,13 @@ public class DictAttribute : ValidationAttribute, ITransient
|
||||
// 判断是否允许空值
|
||||
if (AllowNullValue && value == null) return ValidationResult.Success;
|
||||
|
||||
var valueAsString = value?.ToString();
|
||||
|
||||
// 是否忽略空字符串
|
||||
if (AllowEmptyStrings && string.IsNullOrEmpty(valueAsString)) return ValidationResult.Success;
|
||||
|
||||
// 获取属性的类型
|
||||
var property = validationContext.ObjectType.GetProperty(validationContext.MemberName!);
|
||||
if (property == null) return new ValidationResult($"未知属性: {validationContext.MemberName}");
|
||||
|
||||
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
string importHeaderName = GetImporterHeaderName(property, validationContext.MemberName);
|
||||
|
||||
// 枚举类型验证
|
||||
if (propertyType.IsEnum)
|
||||
{
|
||||
if (!Enum.IsDefined(propertyType, value!)) return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!");
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
|
||||
// 先尝试从 ValidationContext 的依赖注入容器中拿服务,拿不到或类型不匹配时,再从全局的 App 容器中获取
|
||||
if (validationContext.GetService(typeof(SysDictDataService)) is not SysDictDataService sysDictDataService)
|
||||
@ -78,8 +68,71 @@ public class DictAttribute : ValidationAttribute, ITransient
|
||||
// 使用 HashSet 来提高查找效率
|
||||
var dictHash = new HashSet<string>(dictDataList.Select(u => u.Value));
|
||||
|
||||
if (!dictHash.Contains(valueAsString)) return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{valueAsString}】!");
|
||||
// 判断是否为集合类型
|
||||
if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
// 如果是空集合并且允许空值,则直接返回成功
|
||||
if (value == null && AllowNullValue) return ValidationResult.Success;
|
||||
|
||||
// 处理集合为空的情况
|
||||
var collection = value as IEnumerable;
|
||||
if (collection == null) return ValidationResult.Success;
|
||||
|
||||
// 获取集合的元素类型
|
||||
var elementType = propertyType.GetGenericArguments()[0];
|
||||
var underlyingElementType = Nullable.GetUnderlyingType(elementType) ?? elementType;
|
||||
|
||||
// 如果元素类型是枚举,则逐个验证
|
||||
if (underlyingElementType.IsEnum)
|
||||
{
|
||||
foreach (var item in collection)
|
||||
{
|
||||
if (item == null && AllowNullValue) continue;
|
||||
|
||||
if (!Enum.IsDefined(underlyingElementType, item!))
|
||||
return new ValidationResult($"提示:{ErrorMessage}|枚举值【{item}】不是有效的【{underlyingElementType.Name}】枚举类型值!", [importHeaderName]);
|
||||
}
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
foreach (var item in collection)
|
||||
{
|
||||
if (item == null && AllowNullValue) continue;
|
||||
|
||||
var itemString = item?.ToString();
|
||||
if (!dictHash.Contains(itemString))
|
||||
return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{itemString}】!", [importHeaderName]);
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
var valueAsString = value?.ToString();
|
||||
|
||||
// 是否忽略空字符串
|
||||
if (AllowEmptyStrings && string.IsNullOrEmpty(valueAsString)) return ValidationResult.Success;
|
||||
|
||||
// 枚举类型验证
|
||||
if (propertyType.IsEnum)
|
||||
{
|
||||
if (!Enum.IsDefined(propertyType, value!)) return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!", [importHeaderName]);
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
if (!dictHash.Contains(valueAsString))
|
||||
return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{valueAsString}】!", [importHeaderName]);
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本字段上 [ImporterHeader(Name = "xxx")] 里的Name,如果没有则使用defaultName.
|
||||
/// 用于在从excel导入数据时,能让调用者知道是哪个字段验证失败,而不是抛异常
|
||||
/// </summary>
|
||||
private static string GetImporterHeaderName(PropertyInfo property, string defaultName)
|
||||
{
|
||||
var importerHeader = property.GetCustomAttribute<ImporterHeaderAttribute>();
|
||||
string importerHeaderName = importerHeader?.Name ?? defaultName;
|
||||
return importerHeaderName;
|
||||
}
|
||||
}
|
||||
@ -318,7 +318,7 @@ public static class SqlSugarExtension
|
||||
|
||||
#endregion 动态查询扩展方法
|
||||
|
||||
#region 切换数据库
|
||||
#region 初始化库表结构和种子数据
|
||||
|
||||
/// <summary>
|
||||
/// 初始化表实体
|
||||
@ -418,16 +418,16 @@ public static class SqlSugarExtension
|
||||
}
|
||||
|
||||
// 若实体包含Id字段,则设置为当前租户Id递增1
|
||||
var idProp = entityType.GetProperty(nameof(EntityBaseId.Id));
|
||||
var idProperty = entityType.GetProperty(nameof(EntityBaseId.Id));
|
||||
var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType);
|
||||
if (idProp != null && entityInfo.Columns.Any(u => u.PropertyName == nameof(EntityBaseId.Id)))
|
||||
if (idProperty != null && idProperty.PropertyType == typeof(long) && entityInfo.Columns.Any(u => u.PropertyName == nameof(EntityBaseId.Id)))
|
||||
{
|
||||
var seedId = config.ConfigId.ToLong();
|
||||
foreach (var sd in seedData)
|
||||
{
|
||||
var id = idProp!.GetValue(sd, null);
|
||||
var id = idProperty!.GetValue(sd, null);
|
||||
if (id == null || id.ToString() == "0" || string.IsNullOrWhiteSpace(id.ToString()))
|
||||
idProp.SetValue(sd, ++seedId);
|
||||
idProperty.SetValue(sd, ++seedId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -476,7 +476,7 @@ public static class SqlSugarExtension
|
||||
return (total, insertCount, updateCount);
|
||||
}
|
||||
|
||||
#endregion 切换数据库
|
||||
#endregion 初始化库表结构和种子数据
|
||||
|
||||
#region 视图操作
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "admin.net.pro",
|
||||
"type": "module",
|
||||
"version": "2.4.33",
|
||||
"lastBuildTime": "2025.06.10",
|
||||
"lastBuildTime": "2025.06.11",
|
||||
"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
|
||||
"author": "zuohuaijun",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="sys-import-data-container">
|
||||
<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="300px">
|
||||
<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="400px" @close="resetDialog">
|
||||
<template #header>
|
||||
<div style="color: #fff">
|
||||
<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-UploadFilled /> </el-icon>
|
||||
@ -9,21 +9,34 @@
|
||||
</template>
|
||||
|
||||
<el-row :gutter="15" v-loading="state.loading">
|
||||
<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
|
||||
<el-button class="ml10" type="info" icon="ele-Download" v-reclick="3000" @click="() => download()" :disabled="state.loading">{{ t('list.template') }}</el-button>
|
||||
<el-col :span="24" class="mb10">
|
||||
<el-button icon="ele-Download" v-reclick="3000" @click="download" :disabled="state.loading">{{ t('list.template') }}</el-button>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
|
||||
<el-upload :limit="1" :show-file-list="false" :on-exceed="handleExceed" :http-request="handleImportData" ref="uploadRef">
|
||||
|
||||
<el-col :span="24" class="mb15 flex">
|
||||
<el-upload :limit="1" :show-file-list="false" :auto-upload="false" :on-exceed="handleExceed" :on-change="handleFileChange" ref="uploadRef">
|
||||
<template #trigger>
|
||||
<el-button type="primary" icon="ele-MostlyCloudy" v-reclick="3000" :disabled="state.loading">{{ t('list.import') }}</el-button>
|
||||
<el-button class="mr10" type="primary" icon="ele-MostlyCloudy" :disabled="state.isCompleted || state.loading">{{ t('list.import') }}</el-button>
|
||||
</template>
|
||||
</el-upload>
|
||||
<span class="selected-file">{{ state.selectedFile ? state.selectedFile.name : '未选择文件' }}</span>
|
||||
</el-col>
|
||||
|
||||
<!-- 错误提示区域 -->
|
||||
<el-col :span="24" v-if="state.importResultUrl" class="mt10">
|
||||
<div v-if="state.hasError" style="color: red; margin-bottom: 10px">导入完毕,存在部分错误,请下载导入结果查看详情</div>
|
||||
<el-link type="primary" :underline="false" @click="downloadImportResult">
|
||||
<el-icon class="mr5"><ele-Download /></el-icon>下载导入结果
|
||||
</el-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button icon="ele-CircleCloseFilled" @click="() => (state.isShowDialog = false)" :disabled="state.loading">{{ t('list.cancelButtonText') }}</el-button>
|
||||
<el-button icon="ele-CircleCloseFilled" @click="closeDialog" :disabled="state.loading">{{ t('list.cancelButtonText') }}</el-button>
|
||||
<el-button type="primary" @click="state.isCompleted ? closeDialog() : submitImport()" :disabled="(!state.selectedFile && !state.isCompleted) || state.loading">
|
||||
{{ state.isCompleted ? '关 闭' : '确 定' }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -31,7 +44,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="sysImportData">
|
||||
import type { UploadInstance, UploadProps, UploadRawFile, UploadRequestOptions } from 'element-plus';
|
||||
import type { UploadInstance, UploadProps, UploadRawFile, UploadFile } from 'element-plus';
|
||||
import { ElUpload, ElMessage, genFileId } from 'element-plus';
|
||||
import { downloadStreamFile } from '/@/utils/download';
|
||||
import { reactive, ref } from 'vue';
|
||||
@ -42,6 +55,11 @@ const uploadRef = ref<UploadInstance>();
|
||||
const state = reactive({
|
||||
isShowDialog: false,
|
||||
loading: false,
|
||||
isCompleted: false,
|
||||
hasError: false,
|
||||
selectedFile: null as File | null,
|
||||
importResultUrl: '' as string,
|
||||
importResultName: '' as string,
|
||||
});
|
||||
|
||||
// 定义子组件向父组件传值/事件
|
||||
@ -50,9 +68,28 @@ const emit = defineEmits(['refresh']);
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
resetDialog();
|
||||
state.isShowDialog = true;
|
||||
};
|
||||
|
||||
// 重置对话框状态
|
||||
const resetDialog = () => {
|
||||
state.isCompleted = false;
|
||||
state.hasError = false;
|
||||
state.selectedFile = null;
|
||||
state.importResultUrl = '';
|
||||
state.importResultName = '';
|
||||
uploadRef.value?.clearFiles();
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
state.isShowDialog = false;
|
||||
if (state.importResultUrl) {
|
||||
URL.revokeObjectURL(state.importResultUrl);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择文件超出上限事件
|
||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadRef.value!.clearFiles();
|
||||
@ -61,39 +98,87 @@ const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadRef.value!.handleStart(file);
|
||||
};
|
||||
|
||||
// 数据导入
|
||||
const handleImportData = (opt: UploadRequestOptions): any => {
|
||||
state.loading = true;
|
||||
props
|
||||
.import(opt.file)
|
||||
.then((res: any) => {
|
||||
// 返回json数据的情况
|
||||
const contentType = res.headers['content-type'];
|
||||
if (contentType && contentType.toLowerCase().includes('application/json')) {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const data = decoder.decode(res.data);
|
||||
try {
|
||||
const result = JSON.parse(data);
|
||||
if (result.code == '200') {
|
||||
ElMessage.success(result.message);
|
||||
} else {
|
||||
ElMessage.error(result.message);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析数据导入结果失败:', e);
|
||||
downloadStreamFile(res);
|
||||
}
|
||||
// 文件选择变更事件
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
state.selectedFile = file.raw as File;
|
||||
};
|
||||
|
||||
// 提交导入
|
||||
const submitImport = async () => {
|
||||
if (!state.selectedFile) return;
|
||||
|
||||
try {
|
||||
state.loading = true;
|
||||
const res = await props.import(state.selectedFile);
|
||||
|
||||
// 处理导入结果
|
||||
const contentType = res.headers['content-type'] || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
// JSON响应处理(无错误文件)
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const data = decoder.decode(res.data);
|
||||
const result = JSON.parse(data);
|
||||
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(result.message);
|
||||
emit('refresh');
|
||||
state.hasError = false;
|
||||
closeDialog(); // 关键修改:成功导入后直接关闭对话框
|
||||
} else {
|
||||
downloadStreamFile(res);
|
||||
ElMessage.error(result.message);
|
||||
state.hasError = false;
|
||||
}
|
||||
} else {
|
||||
// 二进制响应处理(有错误文件)
|
||||
const blob = new Blob([res.data]);
|
||||
const contentDisposition = res.headers['content-disposition'];
|
||||
let filename = '导入结果.xlsx';
|
||||
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
||||
if (match && match[1]) {
|
||||
filename = decodeURIComponent(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除旧URL
|
||||
if (state.importResultUrl) {
|
||||
URL.revokeObjectURL(state.importResultUrl);
|
||||
}
|
||||
|
||||
// 创建导入结果URL
|
||||
state.importResultUrl = URL.createObjectURL(blob);
|
||||
state.importResultName = filename;
|
||||
state.isCompleted = true;
|
||||
state.hasError = true;
|
||||
|
||||
//刷新列表显示
|
||||
emit('refresh');
|
||||
state.isShowDialog = false;
|
||||
})
|
||||
.finally(() => {
|
||||
uploadRef.value?.clearFiles();
|
||||
state.loading = false;
|
||||
});
|
||||
|
||||
ElMessage.warning('导入完成,存在部分错误');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导入错误:', error);
|
||||
ElMessage.error('导入过程中发生错误');
|
||||
state.hasError = false;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 下载导入结果
|
||||
const downloadImportResult = () => {
|
||||
if (!state.importResultUrl) return;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = state.importResultUrl;
|
||||
link.download = state.importResultName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 关闭对话框(可选,根据需求决定是否保留)
|
||||
// closeDialog();
|
||||
};
|
||||
|
||||
// 下载模板
|
||||
@ -101,9 +186,26 @@ const download = () => {
|
||||
props
|
||||
.download()
|
||||
.then((res: any) => downloadStreamFile(res))
|
||||
.catch((res: any) => ElMessage.error(`${t('list.downloadError')}: ${res}`));
|
||||
.catch((err: any) => ElMessage.error('下载错误: ' + err));
|
||||
};
|
||||
|
||||
// 导出对象
|
||||
defineExpose({ openDialog });
|
||||
defineExpose({ openDialog, closeDialog });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selected-file {
|
||||
margin-left: 10px;
|
||||
line-height: 32px;
|
||||
color: var(--el-text-color-regular);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user