😎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 targetProperty = instance.GetType().GetProperty(PropertyName);
// 判断校验字段内容是否为字典
var dictAttr = targetProperty?.GetCustomAttribute<DictAttribute>();
if (targetProperty == null) return new ValidationResult($"找不到属性: {PropertyName}");
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;
}
@ -78,7 +80,7 @@ public sealed class RequiredIFAttribute(
/// </summary>
/// <param name="targetValue">依赖属性的值</param>
/// <returns>是否需要验证</returns>
private bool ShouldValidate(object targetValue)
private bool ShouldValidate(object targetValue, DictAttribute dictAttr)
{
switch (Comparison)
{
@ -94,8 +96,26 @@ public sealed class RequiredIFAttribute(
case Operator.LessThanOrEqual:
case Operator.Contains:
case Operator.NotContains:
if (targetValue is IEnumerable enumerable) return enumerable.Cast<object>().Any(item => CompareValues(item, 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:
return false;
@ -137,9 +157,28 @@ public sealed class RequiredIFAttribute(
case Operator.Contains:
case Operator.NotContains:
if (targetValue is not IEnumerable enumerable) return false;
bool contains = enumerable.Cast<object>().Any(item => item != null && item.Equals(sourceValue));
if (targetValue == null) return false;
if (sourceValue == null) return comparison == Operator.NotContains;
// 多选字典
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:
return false;

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11012.119 d18.0
# Visual Studio Version 17
VisualStudioVersion = 17.14.36511.14 d17.14
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}"
EndProject

View File

@ -26,7 +26,7 @@
<ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.3.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>

View File

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

View File

@ -1,6 +1,6 @@
<!-- 下拉选择组件支持远程搜索分页自定义查询表单等功能 -->
<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';
const props = defineProps({
@ -258,7 +258,7 @@ const remoteMethod = debounce((query: any) => {
state.tableData.items = [];
state.tableData.total = 0;
}
}, 300);
}, 800);
const handleQuery = () => {
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];
//
if (!props.multiple || state.selectedValues) selectRef.value?.blur();
tableRef.value?.setCurrentRow(row);
//
emit('update:modelValue', state.selectedValues);
emit('change', state.selectedValues, row);
//
selectRef.value?.blur();
//
tableRef.value?.setCurrentRow(row);
};
//
@ -388,7 +391,7 @@ defineExpose({
ref="tableRef"
@row-click="handleChange"
: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
>
<template #empty><el-empty :image-size="25" /></template>
@ -412,8 +415,7 @@ defineExpose({
</div>
</el-select>
</template>
<style scoped lang="scss">
<style scoped>
.query-form {
z-index: 9999;
}
@ -430,7 +432,8 @@ defineExpose({
:deep(.popper-class) :deep(.el-select-dropdown__wrap) {
max-height: 600px !important;
}
</style>
<style>
.popper-class .el-select-dropdown__wrap {
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-option v-for="(item, index) in formattedDictData" :key="index" :label="getDisplayText(item)" :value="item.value" :disabled="item.disabled" />
<slot />
</el-select>
<!-- 多选框多选 -->
<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" />
<slot />
</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">
{{ getDisplayText(item) }}
</el-checkbox-button>
<slot />
</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">
{{ getDisplayText(item) }}
</el-radio>
<slot />
</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">
{{ getDisplayText(item) }}
</el-radio-button>
<slot />
</el-radio-group>
</template>
<style scoped lang="scss"></style>

View File

@ -36,6 +36,7 @@
</el-descriptions-item>
<el-descriptions-item :span="3" label="请求头" class-name="row-line">
<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 :span="3" label="请求参数" class-name="row-line" v-if="data.requestUrl?.indexOf('?') != -1">
<el-row v-for="(value, key, index) in queryObject">
@ -44,22 +45,28 @@
</el-row>
</el-descriptions-item>
<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 :span="3" label="请求明文" class-name="row-line" v-if="data.requestBodyPlaintext">
<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 :span="3" label="响应头" class-name="row-line">
<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 :span="3" label="响应体" class-name="row-line">
<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 :span="3" label="响应明文" class-name="row-line" v-if="data.responseBodyPlaintext">
<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 :span="3" label="异常信息" class-name="row-line" v-if="data.exception">
<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>
</el-form>
@ -76,7 +83,9 @@ import { ref, reactive, computed } from 'vue';
import { StringToObj } from '/@/utils/json-utils';
import { SysLogHttp } from '/@/api-services/system/models';
import VueJsonPretty from 'vue-json-pretty';
import commonFunction from '/@/utils/commonFunction';
const comFunc = commonFunction();
const state = reactive({
visible: false,
});