😎1、优化跨租户文件上传用户所属问题 2、优化条件必填参数验证特性 3、优化前端下来列表和字典组件 4、优化表单自定义验证规则

This commit is contained in:
zuohuaijun 2025-09-28 12:34:41 +08:00
parent be7cd177d1
commit c92881a1ef
10 changed files with 203 additions and 125 deletions

View File

@ -52,6 +52,10 @@ public sealed class RequiredIFAttribute(
/// </summary>
private Operator Comparison { get; set; } = comparison;
public RequiredIFAttribute(string propertyName, object[] targetValues, Operator comparison = Operator.Equal) : this(propertyName, targetValues as object, comparison)
{
}
/// <summary>
/// 验证属性值是否符合要求
/// </summary>

View File

@ -71,6 +71,11 @@ public class UploadFileInput
/// 业务数据Id
/// </summary>
public long DataId { get; set; }
/// <summary>
/// 上传用户Id解决跨租户上传时用户所属不一致问题
/// </summary>
public long UserId { get; set; }
}
/// <summary>
@ -115,4 +120,9 @@ public class UploadFileFromBase64Input
/// 业务Id
/// </summary>
public long? DataId { get; set; }
/// <summary>
/// 上传用户Id解决跨租户上传时用户所属不一致问题
/// </summary>
public long UserId { get; set; }
}

View File

@ -174,12 +174,15 @@ public class SysFileService : IDynamicApiController, ITransient
/// <summary>
/// 下载指定文件Base64格式 🔖
/// </summary>
/// <param name="id"></param>
/// <param name="url"></param>
/// <returns></returns>
[DisplayName("下载指定文件Base64格式")]
public async Task<string> DownloadFileBase64([FromBody] string url)
public async Task<string> GetFileBase64([FromQuery] long id, [FromQuery] string url)
{
var sysFile = await _sysFileRep.AsQueryable().ClearFilter<ITenantIdFilter>().FirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在");
var sysFile = await _sysFileRep.AsQueryable().ClearFilter<ITenantIdFilter>()
.WhereIF(id > 0, u => u.Id == id)
.WhereIF(!string.IsNullOrWhiteSpace(url), u => u.Url == url).FirstAsync() ?? throw Oops.Oh($"文件不存在");
return await _customFileProvider.DownloadFileBase64Async(sysFile);
}
@ -322,8 +325,21 @@ public class SysFileService : IDynamicApiController, ITransient
newFile.FilePath = path;
newFile.FileMd5 = fileMd5;
var finalName = newFile.Id + suffix; // 文件最终名称
// 解决跨租户上传时用户所属不一致问题
if (input.UserId > 0)
{
var user = await _sysFileRep.Context.Queryable<SysUser>().ClearFilter<ITenantIdFilter>().FirstAsync(u => u.Id == input.UserId);
if (user != null)
{
newFile.CreateUserId = user.Id;
newFile.CreateUserName = user.RealName;
newFile.CreateOrgId = user.OrgId;
newFile.CreateOrgName = user.SysOrg?.Name;
newFile.TenantId = user.TenantId;
}
}
var finalName = newFile.Id + suffix; // 文件最终名称
newFile = await _customFileProvider.UploadFileAsync(input.File, newFile, path, finalName);
await _sysFileRep.AsInsertable(newFile).ExecuteCommandAsync();
return newFile;

View File

@ -83,13 +83,13 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
},
/**
*
* @summary Base64格式 🔖
* @param {string} [body]
* @summary Id或Url下载 🔖
* @param {SysFile} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysFileDownloadFileBase64Post: async (body?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysFile/downloadFileBase64`;
apiSysFileDownloadFilePost: async (body?: SysFile, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysFile/downloadFile`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, 'https://example.com');
let baseOptions;
@ -131,20 +131,21 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
},
/**
*
* @summary Id或Url下载 🔖
* @param {SysFile} [body]
* @summary Base64格式 🔖
* @param {number} [id]
* @param {string} [url]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysFileDownloadFilePost: async (body?: SysFile, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysFile/downloadFile`;
apiSysFileFileBase64Get: async (id?: number, url?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysFile/fileBase64`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, 'https://example.com');
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
@ -157,7 +158,13 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
}
localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
if (id !== undefined) {
localVarQueryParameter['id'] = id;
}
if (url !== undefined) {
localVarQueryParameter['url'] = url;
}
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
@ -169,8 +176,6 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
localVarUrlObj.search = (new URLSearchParams(query)).toString();
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
localVarRequestOptions.data = needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
return {
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
@ -634,10 +639,11 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
* @param {boolean} [isPublic]
* @param {string} [allowSuffix]
* @param {number} [dataId]
* @param {number} [userId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysFileUploadFilePostForm: async (file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, dataId?: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
apiSysFileUploadFilePostForm: async (file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, dataId?: number, userId?: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysFile/uploadFile`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, 'https://example.com');
@ -684,6 +690,10 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
localVarFormParams.append('DataId', dataId as any);
}
if (userId !== undefined) {
localVarFormParams.append('UserId', userId as any);
}
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
@ -941,20 +951,6 @@ export const SysFileApiFp = function(configuration?: Configuration) {
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary Base64格式 🔖
* @param {string} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileDownloadFileBase64Post(body?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultString>>> {
const localVarAxiosArgs = await SysFileApiAxiosParamCreator(configuration).apiSysFileDownloadFileBase64Post(body, options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary Id或Url下载 🔖
@ -969,6 +965,21 @@ export const SysFileApiFp = function(configuration?: Configuration) {
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary Base64格式 🔖
* @param {number} [id]
* @param {string} [url]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileFileBase64Get(id?: number, url?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultString>>> {
const localVarAxiosArgs = await SysFileApiAxiosParamCreator(configuration).apiSysFileFileBase64Get(id, url, options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary Id集合获取文件 🔖
@ -1105,11 +1116,12 @@ export const SysFileApiFp = function(configuration?: Configuration) {
* @param {boolean} [isPublic]
* @param {string} [allowSuffix]
* @param {number} [dataId]
* @param {number} [userId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileUploadFilePostForm(file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, dataId?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultSysFile>>> {
const localVarAxiosArgs = await SysFileApiAxiosParamCreator(configuration).apiSysFileUploadFilePostForm(file, fileType, fileAlias, isPublic, allowSuffix, dataId, options);
async apiSysFileUploadFilePostForm(file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, dataId?: number, userId?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultSysFile>>> {
const localVarAxiosArgs = await SysFileApiAxiosParamCreator(configuration).apiSysFileUploadFilePostForm(file, fileType, fileAlias, isPublic, allowSuffix, dataId, userId, options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
@ -1192,16 +1204,6 @@ export const SysFileApiFactory = function (configuration?: Configuration, basePa
async apiSysFileDeletePost(body?: BaseIdInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
return SysFileApiFp(configuration).apiSysFileDeletePost(body, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Base64格式 🔖
* @param {string} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileDownloadFileBase64Post(body?: string, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultString>> {
return SysFileApiFp(configuration).apiSysFileDownloadFileBase64Post(body, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Id或Url下载 🔖
@ -1212,6 +1214,17 @@ export const SysFileApiFactory = function (configuration?: Configuration, basePa
async apiSysFileDownloadFilePost(body?: SysFile, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultIActionResult>> {
return SysFileApiFp(configuration).apiSysFileDownloadFilePost(body, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Base64格式 🔖
* @param {number} [id]
* @param {string} [url]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileFileBase64Get(id?: number, url?: string, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultString>> {
return SysFileApiFp(configuration).apiSysFileFileBase64Get(id, url, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Id集合获取文件 🔖
@ -1312,11 +1325,12 @@ export const SysFileApiFactory = function (configuration?: Configuration, basePa
* @param {boolean} [isPublic]
* @param {string} [allowSuffix]
* @param {number} [dataId]
* @param {number} [userId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileUploadFilePostForm(file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, dataId?: number, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(configuration).apiSysFileUploadFilePostForm(file, fileType, fileAlias, isPublic, allowSuffix, dataId, options).then((request) => request(axios, basePath));
async apiSysFileUploadFilePostForm(file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, dataId?: number, userId?: number, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(configuration).apiSysFileUploadFilePostForm(file, fileType, fileAlias, isPublic, allowSuffix, dataId, userId, options).then((request) => request(axios, basePath));
},
/**
*
@ -1381,17 +1395,6 @@ export class SysFileApi extends BaseAPI {
public async apiSysFileDeletePost(body?: BaseIdInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
return SysFileApiFp(this.configuration).apiSysFileDeletePost(body, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Base64格式 🔖
* @param {string} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysFileApi
*/
public async apiSysFileDownloadFileBase64Post(body?: string, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultString>> {
return SysFileApiFp(this.configuration).apiSysFileDownloadFileBase64Post(body, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Id或Url下载 🔖
@ -1403,6 +1406,18 @@ export class SysFileApi extends BaseAPI {
public async apiSysFileDownloadFilePost(body?: SysFile, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultIActionResult>> {
return SysFileApiFp(this.configuration).apiSysFileDownloadFilePost(body, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Base64格式 🔖
* @param {number} [id]
* @param {string} [url]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysFileApi
*/
public async apiSysFileFileBase64Get(id?: number, url?: string, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultString>> {
return SysFileApiFp(this.configuration).apiSysFileFileBase64Get(id, url, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Id集合获取文件 🔖
@ -1512,12 +1527,13 @@ export class SysFileApi extends BaseAPI {
* @param {boolean} [isPublic]
* @param {string} [allowSuffix]
* @param {number} [dataId]
* @param {number} [userId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysFileApi
*/
public async apiSysFileUploadFilePostForm(file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, dataId?: number, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(this.configuration).apiSysFileUploadFilePostForm(file, fileType, fileAlias, isPublic, allowSuffix, dataId, options).then((request) => request(this.axios, this.basePath));
public async apiSysFileUploadFilePostForm(file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, dataId?: number, userId?: number, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(this.configuration).apiSysFileUploadFilePostForm(file, fileType, fileAlias, isPublic, allowSuffix, dataId, userId, options).then((request) => request(this.axios, this.basePath));
}
/**
*

View File

@ -67,4 +67,12 @@ export interface SysFileUploadFileBody {
* @memberof SysFileUploadFileBody
*/
dataId?: number;
/**
* Id
*
* @type {number}
* @memberof SysFileUploadFileBody
*/
userId?: number;
}

View File

@ -75,4 +75,12 @@ export interface UploadFileFromBase64Input {
* @memberof UploadFileFromBase64Input
*/
dataId?: number | null;
/**
* Id
*
* @type {number}
* @memberof UploadFileFromBase64Input
*/
userId?: number;
}

View File

@ -292,6 +292,8 @@ const handleChange = (row: any) => {
//
const selectVisibleChange = (visible: boolean) => {
if (visible) {
state.tableData.items = [];
state.tableData.total = 0;
state.tableQuery[props.keywordProp] = undefined;
handleQuery();
}

View File

@ -290,26 +290,30 @@ const formattedDictData = computed(() => {
* @computed
* @returns {DictItem|DictItem[]|null} - 当前选中的字典项或字典项数组
*/
const currentDictItems = computed(() => {
const currentDictItems = computed(() => {
// 0
const isEmpty = (val: any) =>
val === null ||
val === undefined ||
(Array.isArray(val) && val.length === 0) ||
(typeof val === 'string' && val.trim() === '');
const isEmpty = (val: any) => val === null || val === undefined || (Array.isArray(val) && val.length === 0) || (typeof val === 'string' && val.trim() === '');
if (isEmpty(state.value)) return null;
let values: any[] = [];
if (props.multiple) {
if (Array.isArray(state.value)) {
// /
const uniqueValues = [...new Set(state.value)];
return formattedDictData.value.filter(item =>
uniqueValues.some(val => val == item.value)
);
values = state.value;
} else if (typeof state.value === 'string' && props.renderAs === 'tag') {
values = state.value.split(',').filter((v) => v !== '');
} else if (state.value !== undefined && state.value !== null && state.value !== '') {
values = [state.value];
}
console.log('[g-sys-dict] 解析多选值:', state.value, values);
//
const uniqueValues = [...new Set(values)];
return formattedDictData.value.filter((item) => uniqueValues.some((val) => val == item.value));
}
//
return formattedDictData.value.find(item => item.value == state.value) || null;
return formattedDictData.value.find((item) => item.value == state.value) || null;
});
/**
@ -376,6 +380,22 @@ const parseMultipleValue = (value: any): any => {
return props.multiple ? [] : value;
}
//
if (props.multiple && typeof value === 'string') {
if (value.trim().startsWith('[') && value.trim().endsWith(']')) {
try {
return JSON.parse(value.trim());
} catch {
return [];
}
}
// +
return value
.split(',')
.map((v) => v.trim())
.filter((v) => v !== '');
}
//
if (typeof value === 'number' && !state.conversion) {
try {
@ -388,29 +408,6 @@ const parseMultipleValue = (value: any): any => {
}
}
//
if (typeof value === 'string') {
const trimmedValue = value.trim();
// JSON
if (trimmedValue.startsWith('[') && trimmedValue.endsWith(']')) {
try {
return JSON.parse(trimmedValue);
} catch (error) {
console.warn('[g-sys-dict] 解析多选值失败:', error);
return [];
}
}
//
if (props.multipleModel === MultipleModel.Comma && trimmedValue.includes(',')) {
return trimmedValue.split(',');
}
//
return props.multiple ? [trimmedValue] : trimmedValue;
}
//
return value;
};
@ -477,11 +474,9 @@ const handleMutex = (newValue: any, mutexConfigs: MutexConfig[]): any => {
* @function
* @param {any} newValue - 新值
*/
const updateValue = (newValue: any) => {
const updateValue = (newValue: any) => {
//
let processedValue = Array.isArray(newValue) ? newValue : (typeof newValue === 'string' && props.multipleModel === MultipleModel.Comma)
? newValue.split(',').filter(Boolean)
: newValue;
let processedValue = Array.isArray(newValue) ? newValue : typeof newValue === 'string' && props.multipleModel === MultipleModel.Comma ? newValue.split(',').filter(Boolean) : newValue;
//
if (props.mutexConfigs && props.mutexConfigs.length > 0) {

View File

@ -135,3 +135,10 @@
.vxe-modal--close-btn:hover {
color: red !important;
}
// 解决el-dialog和vxe-tooltip弹出层z-index冲突问题
.vxe-modal--wrapper,
.vxe-tooltip--wrapper,
.vxe-table--filter-wrapper{
z-index: 2023 !important;
}

View File

@ -31,7 +31,7 @@ class ValidationBuilder {
constructor(config: ValidatorConfig) {
this.prop = config.prop;
this.label = config.label || config.prop;
this.label = config.label || '此项';
this.customMessages = config.customMessages || {};
this.defaultTrigger = config.trigger || 'blur'; // 设置默认触发方式
}
@ -75,6 +75,18 @@ class ValidationBuilder {
return this;
}
// 条件必填验证
requiredIf(required?: boolean, message?: string): ValidationBuilder {
const defaultMessage = `${this.label}不能为空`;
const rule: ValidationRule = {
type: 'required', // 添加 type 字段
message: message || this.getMessage('required', defaultMessage),
required: required,
};
this.rules.push(rule);
return this;
}
// 最小长度验证
min(len: number, message?: string): ValidationBuilder {
const defaultMessage = `${this.label}最少输入${len}个字符`;