😎1、优化条件必填参数验证特性 2、优化字典和下拉框组件 3、优化远程日志页面

This commit is contained in:
zuohuaijun 2025-09-23 13:21:02 +08:00
parent 83ca39fd9f
commit 801a31dfba
7 changed files with 83 additions and 27 deletions

View File

@ -64,11 +64,13 @@ public sealed class RequiredIFAttribute(
var instance = validationContext.ObjectInstance; var instance = validationContext.ObjectInstance;
var targetProperty = instance.GetType().GetProperty(PropertyName); var targetProperty = instance.GetType().GetProperty(PropertyName);
// 判断校验字段内容是否为字典
var dictAttr = targetProperty?.GetCustomAttribute<DictAttribute>();
if (targetProperty == null) return new ValidationResult($"找不到属性: {PropertyName}"); if (targetProperty == null) return new ValidationResult($"找不到属性: {PropertyName}");
var targetValue = targetProperty.GetValue(instance); var targetValue = targetProperty.GetValue(instance);
if (!ShouldValidate(targetValue)) return ValidationResult.Success; if (!ShouldValidate(targetValue, dictAttr)) return ValidationResult.Success;
return IsEmpty(value) ? new ValidationResult(ErrorMessage ?? $"{validationContext.MemberName}不能为空") : ValidationResult.Success; return IsEmpty(value) ? new ValidationResult(ErrorMessage ?? $"{validationContext.MemberName}不能为空") : ValidationResult.Success;
} }
@ -78,7 +80,7 @@ public sealed class RequiredIFAttribute(
/// </summary> /// </summary>
/// <param name="targetValue">依赖属性的值</param> /// <param name="targetValue">依赖属性的值</param>
/// <returns>是否需要验证</returns> /// <returns>是否需要验证</returns>
private bool ShouldValidate(object targetValue) private bool ShouldValidate(object targetValue, DictAttribute dictAttr)
{ {
switch (Comparison) switch (Comparison)
{ {
@ -94,8 +96,26 @@ public sealed class RequiredIFAttribute(
case Operator.LessThanOrEqual: case Operator.LessThanOrEqual:
case Operator.Contains: case Operator.Contains:
case Operator.NotContains: case Operator.NotContains:
if (targetValue is IEnumerable enumerable) return enumerable.Cast<object>().Any(item => CompareValues(item, TargetValue, Comparison)); // 多选字典
return TargetValue == null ? !IsEmpty(targetValue) : CompareValues(targetValue, TargetValue, Comparison); if (dictAttr != null && targetValue is string targetString && targetString.Contains(','))
{
var values = targetString.Split(',')
.Select(v => v.Trim())
.Where(v => !string.IsNullOrEmpty(v))
.ToArray();
return values.Any(value => CompareValues(value, TargetValue, Comparison));
}
// 处理其他集合情况
else if (targetValue is IEnumerable enumerable && !(targetValue is string))
{
return enumerable.Cast<object>().Any(item => CompareValues(item, TargetValue, Comparison));
}
// 单选字典
else
{
return TargetValue == null ? !IsEmpty(targetValue) : CompareValues(targetValue, TargetValue, Comparison);
}
default: default:
return false; return false;
@ -137,9 +157,28 @@ public sealed class RequiredIFAttribute(
case Operator.Contains: case Operator.Contains:
case Operator.NotContains: case Operator.NotContains:
if (targetValue is not IEnumerable enumerable) return false; if (targetValue == null) return false;
bool contains = enumerable.Cast<object>().Any(item => item != null && item.Equals(sourceValue)); if (sourceValue == null) return comparison == Operator.NotContains;
return comparison == Operator.Contains ? contains : !contains;
// 多选字典
if (targetValue is string targetString)
{
string sourceString = sourceValue.ToString();
bool stringContains = targetString.Equals(sourceString);
return comparison == Operator.Contains ? stringContains : !stringContains;
}
// 其他集合类型处理
else if (targetValue is IEnumerable enumerable && !(targetValue is string))
{
bool contains = enumerable.OfType<object>().Any(item =>
item != null && item.Equals(sourceValue));
return comparison == Operator.Contains ? contains : !contains;
}
else
{
bool valuesEqual = targetValue.Equals(sourceValue);
return comparison == Operator.Contains ? valuesEqual : !valuesEqual;
}
default: default:
return false; return false;

View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18 # Visual Studio Version 17
VisualStudioVersion = 18.0.11012.119 d18.0 VisualStudioVersion = 17.14.36511.14 d17.14
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Application", "Admin.NET.Application\Admin.NET.Application.csproj", "{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Application", "Admin.NET.Application\Admin.NET.Application.csproj", "{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}"
EndProject EndProject

View File

@ -26,7 +26,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" /> <PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" />
<PackageReference Include="Rezero.Api" Version="1.8.26" /> <PackageReference Include="Rezero.Api" Version="1.8.27" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -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.09.22", "lastBuildTime": "2025.09.23",
"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架", "description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
"author": "zuohuaijun", "author": "zuohuaijun",
"license": "MIT", "license": "MIT",
@ -93,8 +93,8 @@
"@types/node": "^22.18.6", "@types/node": "^22.18.6",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.44.0", "@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.0", "@typescript-eslint/parser": "^8.44.1",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1", "@vitejs/plugin-vue-jsx": "^5.1.1",
"@vue/compiler-sfc": "^3.5.21", "@vue/compiler-sfc": "^3.5.21",
@ -103,15 +103,15 @@
"colors": "^1.4.0", "colors": "^1.4.0",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"eslint": "^9.36.0", "eslint": "^9.36.0",
"eslint-plugin-vue": "^10.4.0", "eslint-plugin-vue": "^10.5.0",
"globals": "^16.4.0", "globals": "^16.4.0",
"less": "^4.4.1", "less": "^4.4.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"rollup-plugin-visualizer": "^6.0.3", "rollup-plugin-visualizer": "^6.0.3",
"sass": "^1.93.0", "sass": "^1.93.1",
"terser": "^5.44.0", "terser": "^5.44.0",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"vite": "^7.1.6", "vite": "^7.1.7",
"vite-auto-i18n-plugin": "^1.1.9", "vite-auto-i18n-plugin": "^1.1.9",
"vite-plugin-cdn-import": "^1.0.1", "vite-plugin-cdn-import": "^1.0.1",
"vite-plugin-compression2": "^2.2.1", "vite-plugin-compression2": "^2.2.1",

View File

@ -1,6 +1,6 @@
<!-- 下拉选择组件支持远程搜索分页自定义查询表单等功能 --> <!-- 下拉选择组件支持远程搜索分页自定义查询表单等功能 -->
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'; import { inject, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { debounce } from 'xe-utils'; import { debounce } from 'xe-utils';
const props = defineProps({ const props = defineProps({
@ -258,7 +258,7 @@ const remoteMethod = debounce((query: any) => {
state.tableData.items = []; state.tableData.items = [];
state.tableData.total = 0; state.tableData.total = 0;
} }
}, 300); }, 800);
const handleQuery = () => { const handleQuery = () => {
remoteMethod(state.tableQuery); remoteMethod(state.tableQuery);
@ -278,12 +278,15 @@ const handleChange = (row: any) => {
// //
state.selectedValues = props.multiple ? Array.from(new Set([...state.selectedValues, row[props.valueProp]])) : row[props.valueProp]; state.selectedValues = props.multiple ? Array.from(new Set([...state.selectedValues, row[props.valueProp]])) : row[props.valueProp];
// //
if (!props.multiple || state.selectedValues) selectRef.value?.blur();
tableRef.value?.setCurrentRow(row);
emit('update:modelValue', state.selectedValues); emit('update:modelValue', state.selectedValues);
emit('change', state.selectedValues, row); emit('change', state.selectedValues, row);
//
selectRef.value?.blur();
//
tableRef.value?.setCurrentRow(row);
}; };
// //
@ -388,7 +391,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 ? ` - ${queryHeightOffset}px` : ''}${state.tableQuery[keywordProp] && allowCreate ? ` - ${queryHeightOffset}px` : ''})`" :height="`calc(${dropdownHeight} - 175px${$slots.queryForm ? ` - ${queryHeightOffset}px` : ''}${state.tableQuery[keywordProp]})`"
highlight-current-row highlight-current-row
> >
<template #empty><el-empty :image-size="25" /></template> <template #empty><el-empty :image-size="25" /></template>
@ -412,8 +415,7 @@ defineExpose({
</div> </div>
</el-select> </el-select>
</template> </template>
<style scoped>
<style scoped lang="scss">
.query-form { .query-form {
z-index: 9999; z-index: 9999;
} }
@ -430,7 +432,8 @@ defineExpose({
:deep(.popper-class) :deep(.el-select-dropdown__wrap) { :deep(.popper-class) :deep(.el-select-dropdown__wrap) {
max-height: 600px !important; max-height: 600px !important;
} }
</style>
<style>
.popper-class .el-select-dropdown__wrap { .popper-class .el-select-dropdown__wrap {
max-height: 450px !important; max-height: 450px !important;
} }

View File

@ -589,11 +589,13 @@ watch(() => [userStore.dictList, userStore.constList, props.data, state], initDa
<!-- 渲染选择器 --> <!-- 渲染选择器 -->
<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" />
<slot />
</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" />
<slot />
</el-checkbox-group> </el-checkbox-group>
<!-- 多选框-按钮多选 --> <!-- 多选框-按钮多选 -->
@ -601,6 +603,7 @@ watch(() => [userStore.dictList, userStore.constList, props.data, state], initDa
<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>
<slot />
</el-checkbox-group> </el-checkbox-group>
<!-- 渲染单选框 --> <!-- 渲染单选框 -->
@ -608,6 +611,7 @@ watch(() => [userStore.dictList, userStore.constList, props.data, state], initDa
<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>
<slot />
</el-radio-group> </el-radio-group>
<!-- 渲染单选框按钮 --> <!-- 渲染单选框按钮 -->
@ -615,6 +619,7 @@ watch(() => [userStore.dictList, userStore.constList, props.data, state], initDa
<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>
<slot />
</el-radio-group> </el-radio-group>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -36,6 +36,7 @@
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="3" label="请求头" class-name="row-line"> <el-descriptions-item :span="3" label="请求头" class-name="row-line">
<vue-json-pretty :data="data.requestHeaders" showLength showIcon showLineNumber showSelectController /> <vue-json-pretty :data="data.requestHeaders" showLength showIcon showLineNumber showSelectController />
<el-button @click="comFunc.copyText(JSON.stringify(data.requestHeaders))" v-if="data.requestHeaders" style="float: right" icon="ele-CopyDocument" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="3" label="请求参数" class-name="row-line" v-if="data.requestUrl?.indexOf('?') != -1"> <el-descriptions-item :span="3" label="请求参数" class-name="row-line" v-if="data.requestUrl?.indexOf('?') != -1">
<el-row v-for="(value, key, index) in queryObject"> <el-row v-for="(value, key, index) in queryObject">
@ -44,22 +45,28 @@
</el-row> </el-row>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="3" label="请求体" class-name="row-line"> <el-descriptions-item :span="3" label="请求体" class-name="row-line">
<vue-json-pretty :data="data.requestBody" showLength showIcon showLineNumber showSelectController /> <c :data="data.requestBody" showLength showIcon showLineNumber showSelectController />
<el-button @click="comFunc.copyText(JSON.stringify(data.requestBody))" v-if="data.requestBody" style="float: right" icon="ele-CopyDocument" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="3" label="请求明文" class-name="row-line" v-if="data.requestBodyPlaintext"> <el-descriptions-item :span="3" label="请求明文" class-name="row-line" v-if="data.requestBodyPlaintext">
<vue-json-pretty :data="data.requestBodyPlaintext" showLength showIcon showLineNumber showSelectController /> <vue-json-pretty :data="data.requestBodyPlaintext" showLength showIcon showLineNumber showSelectController />
<el-button @click="comFunc.copyText(JSON.stringify(data.requestBodyPlaintext))" v-if="data.requestBodyPlaintext" style="float: right" icon="ele-CopyDocument" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="3" label="响应头" class-name="row-line"> <el-descriptions-item :span="3" label="响应头" class-name="row-line">
<vue-json-pretty :data="data.responseHeaders" showLength showIcon showLineNumber showSelectController /> <vue-json-pretty :data="data.responseHeaders" showLength showIcon showLineNumber showSelectController />
<el-button @click="comFunc.copyText(JSON.stringify(data.responseHeaders))" v-if="data.responseHeaders" style="float: right" icon="ele-CopyDocument" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="3" label="响应体" class-name="row-line"> <el-descriptions-item :span="3" label="响应体" class-name="row-line">
<vue-json-pretty :data="data.responseBody" showLength showIcon showLineNumber showSelectController /> <vue-json-pretty :data="data.responseBody" showLength showIcon showLineNumber showSelectController />
<el-button @click="comFunc.copyText(JSON.stringify(data.responseBody))" v-if="data.responseBody" style="float: right" icon="ele-CopyDocument" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="3" label="响应明文" class-name="row-line" v-if="data.responseBodyPlaintext"> <el-descriptions-item :span="3" label="响应明文" class-name="row-line" v-if="data.responseBodyPlaintext">
<vue-json-pretty :data="data.responseBodyPlaintext" showLength showIcon showLineNumber showSelectController /> <vue-json-pretty :data="data.responseBodyPlaintext" showLength showIcon showLineNumber showSelectController />
<el-button @click="comFunc.copyText(JSON.stringify(data.responseBodyPlaintext))" v-if="data.responseBodyPlaintext" style="float: right" icon="ele-CopyDocument" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="3" label="异常信息" class-name="row-line" v-if="data.exception"> <el-descriptions-item :span="3" label="异常信息" class-name="row-line" v-if="data.exception">
<vue-json-pretty :data="data.exception" showLength showIcon showLineNumber showSelectController /> <vue-json-pretty :data="data.exception" showLength showIcon showLineNumber showSelectController />
<el-button @click="comFunc.copyText(JSON.stringify(data.exception))" v-if="data.exception" style="float: right" icon="ele-CopyDocument" />
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-form> </el-form>
@ -76,7 +83,9 @@ import { ref, reactive, computed } from 'vue';
import { StringToObj } from '/@/utils/json-utils'; import { StringToObj } from '/@/utils/json-utils';
import { SysLogHttp } from '/@/api-services/system/models'; import { SysLogHttp } from '/@/api-services/system/models';
import VueJsonPretty from 'vue-json-pretty'; import VueJsonPretty from 'vue-json-pretty';
import commonFunction from '/@/utils/commonFunction';
const comFunc = commonFunction();
const state = reactive({ const state = reactive({
visible: false, visible: false,
}); });