😎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;
|
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!);
|
var property = validationContext.ObjectType.GetProperty(validationContext.MemberName!);
|
||||||
if (property == null) return new ValidationResult($"未知属性: {validationContext.MemberName}");
|
if (property == null) return new ValidationResult($"未知属性: {validationContext.MemberName}");
|
||||||
|
|
||||||
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
string importHeaderName = GetImporterHeaderName(property, validationContext.MemberName);
|
||||||
|
|
||||||
// 枚举类型验证
|
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||||
if (propertyType.IsEnum)
|
|
||||||
{
|
|
||||||
if (!Enum.IsDefined(propertyType, value!)) return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!");
|
|
||||||
return ValidationResult.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先尝试从 ValidationContext 的依赖注入容器中拿服务,拿不到或类型不匹配时,再从全局的 App 容器中获取
|
// 先尝试从 ValidationContext 的依赖注入容器中拿服务,拿不到或类型不匹配时,再从全局的 App 容器中获取
|
||||||
if (validationContext.GetService(typeof(SysDictDataService)) is not SysDictDataService sysDictDataService)
|
if (validationContext.GetService(typeof(SysDictDataService)) is not SysDictDataService sysDictDataService)
|
||||||
@ -78,8 +68,71 @@ public class DictAttribute : ValidationAttribute, ITransient
|
|||||||
// 使用 HashSet 来提高查找效率
|
// 使用 HashSet 来提高查找效率
|
||||||
var dictHash = new HashSet<string>(dictDataList.Select(u => u.Value));
|
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;
|
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 动态查询扩展方法
|
#endregion 动态查询扩展方法
|
||||||
|
|
||||||
#region 切换数据库
|
#region 初始化库表结构和种子数据
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化表实体
|
/// 初始化表实体
|
||||||
@ -418,16 +418,16 @@ public static class SqlSugarExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 若实体包含Id字段,则设置为当前租户Id递增1
|
// 若实体包含Id字段,则设置为当前租户Id递增1
|
||||||
var idProp = entityType.GetProperty(nameof(EntityBaseId.Id));
|
var idProperty = entityType.GetProperty(nameof(EntityBaseId.Id));
|
||||||
var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType);
|
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();
|
var seedId = config.ConfigId.ToLong();
|
||||||
foreach (var sd in seedData)
|
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()))
|
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);
|
return (total, insertCount, updateCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion 切换数据库
|
#endregion 初始化库表结构和种子数据
|
||||||
|
|
||||||
#region 视图操作
|
#region 视图操作
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "admin.net.pro",
|
"name": "admin.net.pro",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.4.33",
|
"version": "2.4.33",
|
||||||
"lastBuildTime": "2025.06.10",
|
"lastBuildTime": "2025.06.11",
|
||||||
"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
|
"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
|
||||||
"author": "zuohuaijun",
|
"author": "zuohuaijun",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sys-import-data-container">
|
<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>
|
<template #header>
|
||||||
<div style="color: #fff">
|
<div style="color: #fff">
|
||||||
<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-UploadFilled /> </el-icon>
|
<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-UploadFilled /> </el-icon>
|
||||||
@ -9,21 +9,34 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-row :gutter="15" v-loading="state.loading">
|
<el-row :gutter="15" v-loading="state.loading">
|
||||||
<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
|
<el-col :span="24" class="mb10">
|
||||||
<el-button class="ml10" type="info" icon="ele-Download" v-reclick="3000" @click="() => download()" :disabled="state.loading">{{ t('list.template') }}</el-button>
|
<el-button icon="ele-Download" v-reclick="3000" @click="download" :disabled="state.loading">{{ t('list.template') }}</el-button>
|
||||||
</el-col>
|
</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>
|
<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>
|
</template>
|
||||||
</el-upload>
|
</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-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-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>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@ -31,7 +44,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup name="sysImportData">
|
<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 { ElUpload, ElMessage, genFileId } from 'element-plus';
|
||||||
import { downloadStreamFile } from '/@/utils/download';
|
import { downloadStreamFile } from '/@/utils/download';
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
@ -42,6 +55,11 @@ const uploadRef = ref<UploadInstance>();
|
|||||||
const state = reactive({
|
const state = reactive({
|
||||||
isShowDialog: false,
|
isShowDialog: false,
|
||||||
loading: 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 = () => {
|
const openDialog = () => {
|
||||||
|
resetDialog();
|
||||||
state.isShowDialog = true;
|
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) => {
|
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||||
uploadRef.value!.clearFiles();
|
uploadRef.value!.clearFiles();
|
||||||
@ -61,39 +98,87 @@ const handleExceed: UploadProps['onExceed'] = (files) => {
|
|||||||
uploadRef.value!.handleStart(file);
|
uploadRef.value!.handleStart(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 数据导入
|
// 文件选择变更事件
|
||||||
const handleImportData = (opt: UploadRequestOptions): any => {
|
const handleFileChange = (file: UploadFile) => {
|
||||||
state.loading = true;
|
state.selectedFile = file.raw as File;
|
||||||
props
|
};
|
||||||
.import(opt.file)
|
|
||||||
.then((res: any) => {
|
// 提交导入
|
||||||
// 返回json数据的情况
|
const submitImport = async () => {
|
||||||
const contentType = res.headers['content-type'];
|
if (!state.selectedFile) return;
|
||||||
if (contentType && contentType.toLowerCase().includes('application/json')) {
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
try {
|
||||||
const data = decoder.decode(res.data);
|
state.loading = true;
|
||||||
try {
|
const res = await props.import(state.selectedFile);
|
||||||
const result = JSON.parse(data);
|
|
||||||
if (result.code == '200') {
|
// 处理导入结果
|
||||||
ElMessage.success(result.message);
|
const contentType = res.headers['content-type'] || '';
|
||||||
} else {
|
if (contentType.includes('application/json')) {
|
||||||
ElMessage.error(result.message);
|
// JSON响应处理(无错误文件)
|
||||||
return;
|
const decoder = new TextDecoder('utf-8');
|
||||||
}
|
const data = decoder.decode(res.data);
|
||||||
} catch (e) {
|
const result = JSON.parse(data);
|
||||||
console.error('解析数据导入结果失败:', e);
|
|
||||||
downloadStreamFile(res);
|
if (result.code === 200) {
|
||||||
}
|
ElMessage.success(result.message);
|
||||||
|
emit('refresh');
|
||||||
|
state.hasError = false;
|
||||||
|
closeDialog(); // 关键修改:成功导入后直接关闭对话框
|
||||||
} else {
|
} 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');
|
emit('refresh');
|
||||||
state.isShowDialog = false;
|
|
||||||
})
|
ElMessage.warning('导入完成,存在部分错误');
|
||||||
.finally(() => {
|
}
|
||||||
uploadRef.value?.clearFiles();
|
} catch (error) {
|
||||||
state.loading = false;
|
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
|
props
|
||||||
.download()
|
.download()
|
||||||
.then((res: any) => downloadStreamFile(res))
|
.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>
|
</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