😎1、统一系统logo文件上传模式 2、增加轮播图上传管理 3、ai组件改成按需加载 4、升级依赖

This commit is contained in:
zuohuaijun 2025-07-28 13:06:28 +08:00
parent ad69c7e5b5
commit 17c3fb6a98
21 changed files with 474 additions and 395 deletions

View File

@ -4,7 +4,7 @@
"Upload": {
"Path": "upload/{yyyy}/{MM}/{dd}", //
"MaxSize": 51200, // KB1024*50
"ContentType": [ "image/jpg", "image/png", "image/jpeg", "image/gif", "image/bmp", "text/plain", "text/xml", "application/pdf", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "video/mp4", "application/wps-office.docx", "application/wps-office.xlsx", "application/wps-office.pptx", "application/vnd.android.package-archive", "application/octet-stream" ],
"ContentType": [ "image/jpg", "image/png", "image/jpeg", "image/gif", "image/bmp", "image/svg+xml", "text/plain", "text/xml", "application/pdf", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "video/mp4", "application/wps-office.docx", "application/wps-office.xlsx", "application/wps-office.pptx", "application/vnd.android.package-archive", "application/octet-stream" ],
"EnableMd5": false, // MDF5-
"EnableSaveDb": false //
},

View File

@ -28,9 +28,9 @@
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" Aliases="BouncyCastleV2" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.0.7" />
<PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.7.105" />
<PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.7.105" />
<PackageReference Include="Furion.Pure" Version="4.9.7.105" />
<PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.7.106" />
<PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.9.7.106" />
<PackageReference Include="Furion.Pure" Version="4.9.7.106" />
<PackageReference Include="Hardware.Info" Version="101.0.1.1" />
<PackageReference Include="Hashids.net" Version="1.7.0" />
<PackageReference Include="IPTools.China" Version="1.6.0" />
@ -74,7 +74,7 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="AspNet.Security.OAuth.Gitee" Version="8.3.0" />
<PackageReference Include="AspNet.Security.OAuth.Weixin" Version="8.3.0" />
<PackageReference Include="Lazy.Captcha.Core" Version="2.2.0" />
<PackageReference Include="Lazy.Captcha.Core" Version="2.2.1" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.11" />

View File

@ -66,6 +66,11 @@ public class UploadFileInput
/// </summary>
/// <example></example>
public string AllowSuffix { get; set; }
/// <summary>
/// 业务数据Id
/// </summary>
public long DataId { get; set; }
}
/// <summary>

View File

@ -21,7 +21,7 @@ public class SysFileService : IDynamicApiController, ITransient
private readonly UploadOptions _uploadOptions;
private readonly INamedServiceProvider<ICustomFileProvider> _namedServiceProvider;
private readonly ICustomFileProvider _customFileProvider;
private readonly string _imageType = ".jpeg.jpg.png.bmp.gif.tif";
private readonly string _imageType = ".jpeg.jpg.png.bmp.gif.tif.svg";
public SysFileService(UserManager userManager,
SqlSugarRepository<SysFile> sysFileRep,
@ -373,6 +373,41 @@ public class SysFileService : IDynamicApiController, ITransient
return sysFile;
}
/// <summary>
/// 上传Logo 🔖
/// </summary>
/// <param name="file"></param>
/// <param name="tenantId"></param>
/// <returns></returns>
[DisplayName("上传Logo")]
public async Task<SysFile> UploadLogo([Required] IFormFile file, [Required] long tenantId)
{
// 先清空
var sysFile = await _sysFileRep.GetFirstAsync(u => u.FileType == "Logo" && u.DataId == tenantId);
if (sysFile != null)
await DeleteFile(new BaseIdInput { Id = sysFile.Id });
return await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType, FileType = "Logo", DataId = tenantId }, $"upload/system/{tenantId}");
}
/// <summary>
/// 上传轮播图 🔖
/// </summary>
/// <param name="files"></param>
/// <param name="tenantId"></param>
/// <returns></returns>
[DisplayName("上传轮播图")]
public async Task<List<SysFile>> UploadCarousel([Required] List<IFormFile> files, [Required] long tenantId)
{
var sysFileList = new List<SysFile>();
foreach (var file in files)
{
var tFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType, FileType = "Carousel", DataId = tenantId }, $"upload/system/{tenantId}");
sysFileList.Add(tFile);
}
return sysFileList;
}
#region SysFile集合导航属性
/// <summary>

View File

@ -17,14 +17,9 @@ public class SysInfoInput
public long TenantId { get; set; }
/// <summary>
/// 图标Data URI scheme base64 编码)
/// 系统图标
/// </summary>
public string LogoBase64 { get; set; }
/// <summary>
/// 图标文件名
/// </summary>
public string LogoFileName { get; set; }
public string Logo { get; set; }
/// <summary>
/// 主标题
@ -99,7 +94,7 @@ public class SysInfoInput
public bool SecondVer { get; set; } = false;
/// <summary>
/// 轮播图
/// 轮播图Id集合
/// </summary>
public List<IFormFile> CarouselFiles { get; set; }
public List<long> CarouselFileIds { get; set; }
}

View File

@ -26,7 +26,6 @@ public class SysTenantService : IDynamicApiController, ITransient
private readonly SysRoleMenuService _sysRoleMenuService;
private readonly SysConfigService _sysConfigService;
private readonly SysCacheService _sysCacheService;
private readonly SysFileService _sysFileService;
private readonly IEventPublisher _eventPublisher;
public SysTenantService(SqlSugarRepository<SysTenant> sysTenantRep,
@ -43,7 +42,6 @@ public class SysTenantService : IDynamicApiController, ITransient
SysRoleMenuService sysRoleMenuService,
SysConfigService sysConfigService,
SysCacheService sysCacheService,
SysFileService sysFileService,
IEventPublisher eventPublisher)
{
_sysTenantRep = sysTenantRep;
@ -60,7 +58,6 @@ public class SysTenantService : IDynamicApiController, ITransient
_sysRoleMenuService = sysRoleMenuService;
_sysConfigService = sysConfigService;
_sysCacheService = sysCacheService;
_sysFileService = sysFileService;
_eventPublisher = eventPublisher;
}
@ -571,8 +568,8 @@ public class SysTenantService : IDynamicApiController, ITransient
if (string.IsNullOrWhiteSpace(tenant.Title))
tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == SqlSugarConst.DefaultTenantId);
//// 获取首页轮播图列表
//var carouselFiles = await _fileRep.GetListAsync(u => u.BelongId == tenant.Id && u.RelationId == tenant.Id && u.FileType == "Carousel");
// 获取首页轮播图列表
var carouselFiles = await _sysTenantRep.ChangeRepository<SqlSugarRepository<SysFile>>().GetListAsync(u => u.DataId == tenant.Id && u.FileType == "Carousel");
var forceChangePassword = await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysForceChangePassword); // 强制修改密码
var passwordExpirationTime = await _sysConfigService.GetConfigValueByCode<int>(ConfigConst.SysPasswordExpirationTime); // 密码有效期
@ -601,7 +598,7 @@ public class SysTenantService : IDynamicApiController, ITransient
ForceChangePassword = forceChangePassword,
PasswordExpirationTime = passwordExpirationTime,
PublicKey = publicKey,
//CarouselFiles = carouselFiles,
CarouselFiles = carouselFiles,
I18NSwitch = i18NSwitch,
IdleTimeout = idleTimeout,
OnlineNotice = onlineNotice,
@ -615,53 +612,19 @@ public class SysTenantService : IDynamicApiController, ITransient
[UnitOfWork]
[DisplayName("保存系统信息")]
public async Task SaveSysInfo(SysInfoInput input)
{
if (input.TenantId < 1)
input.TenantId = long.Parse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? "0");
var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == input.TenantId);
_ = tenant ?? throw Oops.Oh(ErrorCodeEnum.D1002);
var originLogo = tenant.Logo;
{
input.TenantId = input.TenantId < 1 ? long.Parse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? "0") : input.TenantId;
var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == input.TenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
tenant = input.Adapt<SysTenant>();
tenant.Id = input.TenantId;
// logo 不为空才保存
if (!string.IsNullOrEmpty(input.LogoBase64))
{
// 旧图标文件相对路径
var oldSysLogoRelativeFilePath = tenant.Logo ?? "";
var oldSysLogoAbsoluteFilePath = Path.Combine(App.WebHostEnvironment.WebRootPath, oldSysLogoRelativeFilePath.TrimStart('/'));
var groups = Regex.Match(input.LogoBase64, @"data:image/(?<type>.+?);base64,(?<data>.+)").Groups;
//var type = groups["type"].Value;
var base64Data = groups["data"].Value;
var binData = Convert.FromBase64String(base64Data);
// 根据文件名取扩展名
var ext = string.IsNullOrWhiteSpace(input.LogoFileName) ? ".png" : Path.GetExtension(input.LogoFileName);
// 本地图标保存路径
var path = $"upload/{input.TenantId}/";
var fileName = $"logo{ext}".ToLower();
var absoluteFilePath = Path.Combine(App.WebHostEnvironment.WebRootPath, path, fileName);
// 删除已存在文件
if (File.Exists(oldSysLogoAbsoluteFilePath))
File.Delete(oldSysLogoAbsoluteFilePath);
// 创建文件夹
var absoluteFileDir = Path.GetDirectoryName(absoluteFilePath);
if (!Directory.Exists(absoluteFileDir))
Directory.CreateDirectory(absoluteFileDir);
// 保存图标文件
await File.WriteAllBytesAsync(absoluteFilePath, binData);
// 保存图标配置
tenant.Logo = $"/{path}/{fileName}";
}
else
{
tenant.Logo = originLogo;
}
tenant.Id = input.TenantId;
// 先清空轮播图再更新
var carouselFileIds = await _sysTenantRep.ChangeRepository<SqlSugarRepository<SysFile>>().AsQueryable()
.WhereIF(input.CarouselFileIds != null && input.CarouselFileIds.Count != 0, u => !input.CarouselFileIds.Contains(u.Id))
.Where(u => u.FileType == "Carousel" && u.DataId == input.TenantId)
.Select(u => u.Id).ToListAsync();
foreach (var fileId in carouselFileIds)
await App.GetRequiredService<SysFileService>().DeleteFile(new BaseIdInput { Id = fileId });
await _sysTenantRep.AsUpdateable(tenant).UpdateColumns(u => new
{
@ -684,40 +647,4 @@ public class SysTenantService : IDynamicApiController, ITransient
// 更新租户缓存
await CacheTenant();
}
/// <summary>
/// 上传轮播图单文件 🔖
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
[DisplayName("上传轮播图单文件")]
public async Task<SysFile> UploadCarouselFile([Required] IFormFile file)
{
var tenantId = long.Parse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? "0");
if (tenantId < 1) tenantId = SqlSugarConst.DefaultTenantId;
var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == tenantId);
if (tenant == null) return null;
if (file == null)
throw Oops.Oh(ErrorCodeEnum.D8000);
// 本地轮播图保存路径
var path = $"upload/{tenantId}/carousel";
var absoluteDirPath = Path.Combine(App.WebHostEnvironment.WebRootPath, path);
// 创建文件夹
if (!Directory.Exists(absoluteDirPath))
Directory.CreateDirectory(absoluteDirPath);
// 保存轮播图文件
var sysFile = await _sysFileService.UploadFile(new UploadFileInput { File = file, FileType = "Carousel" });
//// 保存轮播图配置
//sysFile.BelongId = tenant.Id;
//sysFile.RelationId = tenant.Id;
await _sysFileService.UpdateFile(sysFile);
return sysFile;
}
}

View File

@ -2,7 +2,7 @@
"name": "admin.net.pro",
"type": "module",
"version": "2.4.33",
"lastBuildTime": "2025.07.27",
"lastBuildTime": "2025.07.28",
"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
"author": "zuohuaijun",
"license": "MIT",
@ -74,7 +74,7 @@
"vue-clipboard3": "^2.0.0",
"vue-demi": "0.14.10",
"vue-draggable-plus": "^0.6.0",
"vue-element-plus-x": "^1.3.0",
"vue-element-plus-x": "^1.3.1",
"vue-grid-layout": "3.0.0-beta1",
"vue-i18n": "^11.1.11",
"vue-json-pretty": "^2.5.0",
@ -82,8 +82,8 @@
"vue-router": "^4.5.1",
"vue-signature-pad": "^3.0.2",
"vue3-tree-org": "^4.2.2",
"vxe-pc-ui": "^4.7.28",
"vxe-table": "^4.14.7",
"vxe-pc-ui": "^4.7.30",
"vxe-table": "^4.14.8",
"xe-utils": "^3.7.8",
"xlsx-js-style": "^1.2.0"
},
@ -96,10 +96,10 @@
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vue/compiler-sfc": "^3.5.18",
"code-inspector-plugin": "^1.0.0",
"code-inspector-plugin": "^1.0.2",
"eslint": "^9.32.0",
"eslint-plugin-vue": "^10.3.0",
"globals": "^16.3.0",

View File

@ -518,6 +518,65 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
options: localVarRequestOptions,
};
},
/**
*
* @summary 🔖
* @param {number} tenantId
* @param {Array<Blob>} [files]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysFileUploadCarouselTenantIdPostForm: async (tenantId: number, files?: Array<Blob>, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'tenantId' is not null or undefined
if (tenantId === null || tenantId === undefined) {
throw new RequiredError('tenantId','Required parameter tenantId was null or undefined when calling apiSysFileUploadCarouselTenantIdPostForm.');
}
const localVarPath = `/api/sysFile/uploadCarousel/{tenantId}`
.replace(`{${"tenantId"}}`, encodeURIComponent(String(tenantId)));
// 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 localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
const localVarFormParams = new FormData();
// authentication Bearer required
// http bearer authentication required
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
}
if (files) {
files.forEach((element) => {
localVarFormParams.append('files', element as any);
})
}
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
query.set(key, localVarQueryParameter[key]);
}
for (const key in options.params) {
query.set(key, options.params[key]);
}
localVarUrlObj.search = (new URLSearchParams(query)).toString();
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = localVarFormParams;
return {
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
options: localVarRequestOptions,
};
},
/**
*
* @summary Base64 🔖
@ -574,10 +633,11 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
* @param {string} [fileAlias]
* @param {boolean} [isPublic]
* @param {string} [allowSuffix]
* @param {number} [dataId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysFileUploadFilePostForm: async (file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
apiSysFileUploadFilePostForm: async (file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, dataId?: 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');
@ -620,6 +680,10 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
localVarFormParams.append('AllowSuffix', allowSuffix as any);
}
if (dataId !== undefined) {
localVarFormParams.append('DataId', dataId as any);
}
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
@ -691,6 +755,64 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
options: localVarRequestOptions,
};
},
/**
*
* @summary Logo 🔖
* @param {number} tenantId
* @param {Blob} [file]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysFileUploadLogoTenantIdPostForm: async (tenantId: number, file?: Blob, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'tenantId' is not null or undefined
if (tenantId === null || tenantId === undefined) {
throw new RequiredError('tenantId','Required parameter tenantId was null or undefined when calling apiSysFileUploadLogoTenantIdPostForm.');
}
const localVarPath = `/api/sysFile/uploadLogo/{tenantId}`
.replace(`{${"tenantId"}}`, encodeURIComponent(String(tenantId)));
// 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 localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
const localVarFormParams = new FormData();
// authentication Bearer required
// http bearer authentication required
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
}
if (file !== undefined) {
localVarFormParams.append('file', file as any);
}
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
query.set(key, localVarQueryParameter[key]);
}
for (const key in options.params) {
query.set(key, options.params[key]);
}
localVarUrlObj.search = (new URLSearchParams(query)).toString();
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = localVarFormParams;
return {
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
options: localVarRequestOptions,
};
},
/**
*
* @summary 🔖
@ -945,6 +1067,21 @@ export const SysFileApiFp = function(configuration?: Configuration) {
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 🔖
* @param {number} tenantId
* @param {Array<Blob>} [files]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileUploadCarouselTenantIdPostForm(tenantId: number, files?: Array<Blob>, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultListSysFile>>> {
const localVarAxiosArgs = await SysFileApiAxiosParamCreator(configuration).apiSysFileUploadCarouselTenantIdPostForm(tenantId, files, options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary Base64 🔖
@ -967,11 +1104,12 @@ export const SysFileApiFp = function(configuration?: Configuration) {
* @param {string} [fileAlias]
* @param {boolean} [isPublic]
* @param {string} [allowSuffix]
* @param {number} [dataId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileUploadFilePostForm(file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultSysFile>>> {
const localVarAxiosArgs = await SysFileApiAxiosParamCreator(configuration).apiSysFileUploadFilePostForm(file, fileType, fileAlias, isPublic, allowSuffix, options);
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);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
@ -991,6 +1129,21 @@ export const SysFileApiFp = function(configuration?: Configuration) {
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary Logo 🔖
* @param {number} tenantId
* @param {Blob} [file]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileUploadLogoTenantIdPostForm(tenantId: number, file?: Blob, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultSysFile>>> {
const localVarAxiosArgs = await SysFileApiAxiosParamCreator(configuration).apiSysFileUploadLogoTenantIdPostForm(tenantId, file, options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 🔖
@ -1129,6 +1282,17 @@ export const SysFileApiFactory = function (configuration?: Configuration, basePa
async apiSysFileUploadAvatarPostForm(file?: Blob, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(configuration).apiSysFileUploadAvatarPostForm(file, options).then((request) => request(axios, basePath));
},
/**
*
* @summary 🔖
* @param {number} tenantId
* @param {Array<Blob>} [files]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileUploadCarouselTenantIdPostForm(tenantId: number, files?: Array<Blob>, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultListSysFile>> {
return SysFileApiFp(configuration).apiSysFileUploadCarouselTenantIdPostForm(tenantId, files, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Base64 🔖
@ -1147,11 +1311,12 @@ export const SysFileApiFactory = function (configuration?: Configuration, basePa
* @param {string} [fileAlias]
* @param {boolean} [isPublic]
* @param {string} [allowSuffix]
* @param {number} [dataId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileUploadFilePostForm(file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(configuration).apiSysFileUploadFilePostForm(file, fileType, fileAlias, isPublic, allowSuffix, options).then((request) => request(axios, basePath));
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));
},
/**
*
@ -1163,6 +1328,17 @@ export const SysFileApiFactory = function (configuration?: Configuration, basePa
async apiSysFileUploadFilesPostForm(files?: Array<Blob>, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultListSysFile>> {
return SysFileApiFp(configuration).apiSysFileUploadFilesPostForm(files, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Logo 🔖
* @param {number} tenantId
* @param {Blob} [file]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysFileUploadLogoTenantIdPostForm(tenantId: number, file?: Blob, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(configuration).apiSysFileUploadLogoTenantIdPostForm(tenantId, file, options).then((request) => request(axios, basePath));
},
/**
*
* @summary 🔖
@ -1304,6 +1480,18 @@ export class SysFileApi extends BaseAPI {
public async apiSysFileUploadAvatarPostForm(file?: Blob, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(this.configuration).apiSysFileUploadAvatarPostForm(file, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 🔖
* @param {number} tenantId
* @param {Array<Blob>} [files]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysFileApi
*/
public async apiSysFileUploadCarouselTenantIdPostForm(tenantId: number, files?: Array<Blob>, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultListSysFile>> {
return SysFileApiFp(this.configuration).apiSysFileUploadCarouselTenantIdPostForm(tenantId, files, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Base64 🔖
@ -1323,12 +1511,13 @@ export class SysFileApi extends BaseAPI {
* @param {string} [fileAlias]
* @param {boolean} [isPublic]
* @param {string} [allowSuffix]
* @param {number} [dataId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysFileApi
*/
public async apiSysFileUploadFilePostForm(file?: Blob, fileType?: string, fileAlias?: string, isPublic?: boolean, allowSuffix?: string, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(this.configuration).apiSysFileUploadFilePostForm(file, fileType, fileAlias, isPublic, allowSuffix, options).then((request) => request(this.axios, this.basePath));
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));
}
/**
*
@ -1341,6 +1530,18 @@ export class SysFileApi extends BaseAPI {
public async apiSysFileUploadFilesPostForm(files?: Array<Blob>, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultListSysFile>> {
return SysFileApiFp(this.configuration).apiSysFileUploadFilesPostForm(files, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Logo 🔖
* @param {number} tenantId
* @param {Blob} [file]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysFileApi
*/
public async apiSysFileUploadLogoTenantIdPostForm(tenantId: number, file?: Blob, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysFileApiFp(this.configuration).apiSysFileUploadLogoTenantIdPostForm(tenantId, file, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 🔖

View File

@ -25,7 +25,6 @@ import { AdminNETResultListSysUser } from '../models';
import { AdminNETResultObject } from '../models';
import { AdminNETResultSqlSugarPagedListTenantOutput } from '../models';
import { AdminNETResultString } from '../models';
import { AdminNETResultSysFile } from '../models';
import { DeleteTenantInput } from '../models';
import { PageTenantInput } from '../models';
import { RoleMenuInput } from '../models';
@ -703,58 +702,6 @@ export const SysTenantApiAxiosParamCreator = function (configuration?: Configura
options: localVarRequestOptions,
};
},
/**
*
* @summary 🔖
* @param {Blob} [file]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiSysTenantUploadCarouselFilePostForm: async (file?: Blob, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/sysTenant/uploadCarouselFile`;
// 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 localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
const localVarFormParams = new FormData();
// authentication Bearer required
// http bearer authentication required
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
}
if (file !== undefined) {
localVarFormParams.append('file', file as any);
}
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
const query = new URLSearchParams(localVarUrlObj.search);
for (const key in localVarQueryParameter) {
query.set(key, localVarQueryParameter[key]);
}
for (const key in options.params) {
query.set(key, options.params[key]);
}
localVarUrlObj.search = (new URLSearchParams(query)).toString();
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = localVarFormParams;
return {
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
options: localVarRequestOptions,
};
},
/**
*
* @summary 🔖
@ -1006,20 +953,6 @@ export const SysTenantApiFp = function(configuration?: Configuration) {
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 🔖
* @param {Blob} [file]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysTenantUploadCarouselFilePostForm(file?: Blob, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultSysFile>>> {
const localVarAxiosArgs = await SysTenantApiAxiosParamCreator(configuration).apiSysTenantUploadCarouselFilePostForm(file, options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
/**
*
* @summary 🔖
@ -1181,16 +1114,6 @@ export const SysTenantApiFactory = function (configuration?: Configuration, base
async apiSysTenantUpdatePost(body?: UpdateTenantInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
return SysTenantApiFp(configuration).apiSysTenantUpdatePost(body, options).then((request) => request(axios, basePath));
},
/**
*
* @summary 🔖
* @param {Blob} [file]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiSysTenantUploadCarouselFilePostForm(file?: Blob, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysTenantApiFp(configuration).apiSysTenantUploadCarouselFilePostForm(file, options).then((request) => request(axios, basePath));
},
/**
*
* @summary 🔖
@ -1363,17 +1286,6 @@ export class SysTenantApi extends BaseAPI {
public async apiSysTenantUpdatePost(body?: UpdateTenantInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
return SysTenantApiFp(this.configuration).apiSysTenantUpdatePost(body, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 🔖
* @param {Blob} [file]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SysTenantApi
*/
public async apiSysTenantUploadCarouselFilePostForm(file?: Blob, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultSysFile>> {
return SysTenantApiFp(this.configuration).apiSysTenantUploadCarouselFilePostForm(file, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary 🔖

View File

@ -503,7 +503,6 @@ export * from './sys-report-group';
export * from './sys-report-layout-config';
export * from './sys-report-param';
export * from './sys-schedule';
export * from './sys-tenant-upload-carousel-file-body';
export * from './sys-upgrade';
export * from './sys-user';
export * from './sys-user-ext-org';
@ -553,7 +552,9 @@ export * from './update-schedule-input';
export * from './update-sys-ldap-input';
export * from './update-tenant-input';
export * from './update-user-input';
export * from './upload-carousel-tenant-id-body';
export * from './upload-file-from-base64-input';
export * from './upload-logo-tenant-id-body';
export * from './user-menu-input';
export * from './user-output';
export * from './user-role-input';

View File

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

View File

@ -29,20 +29,12 @@ export interface SysInfoInput {
tenantId?: number;
/**
* Data URI scheme base64
*
*
* @type {string}
* @memberof SysInfoInput
*/
logoBase64?: string | null;
/**
*
*
* @type {string}
* @memberof SysInfoInput
*/
logoFileName?: string | null;
logo?: string | null;
/**
*
@ -149,10 +141,10 @@ export interface SysInfoInput {
secondVer?: boolean;
/**
*
* Id集合
*
* @type {Array<Blob>}
* @type {Array<number>}
* @memberof SysInfoInput
*/
carouselFiles?: Array<Blob> | null;
carouselFileIds?: Array<number> | null;
}

View File

@ -0,0 +1,28 @@
/* tslint:disable */
/* eslint-disable */
/**
* Admin.NET
* .NET <br/><u><b><font color='FF0000'> 👮</font></b></u>
*
* OpenAPI spec version: 1.0.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
*
*
* @export
* @interface UploadCarouselTenantIdBody
*/
export interface UploadCarouselTenantIdBody {
/**
* @type {Array<Blob>}
* @memberof UploadCarouselTenantIdBody
*/
files: Array<Blob>;
}

View File

@ -16,13 +16,13 @@
*
*
* @export
* @interface SysTenantUploadCarouselFileBody
* @interface UploadLogoTenantIdBody
*/
export interface SysTenantUploadCarouselFileBody {
export interface UploadLogoTenantIdBody {
/**
* @type {Blob}
* @memberof SysTenantUploadCarouselFileBody
* @memberof UploadLogoTenantIdBody
*/
file: Blob;
}

View File

@ -27,7 +27,7 @@ interface iVxeOption {
sortConfig?: VxeTablePropTypes.SortConfig<any>;
showFooter?: boolean;
footerData?: VxeTablePropTypes.FooterData;
footerMethod?: VxeTablePropTypes.FooterMethod<D>;
footerMethod?: VxeTablePropTypes.FooterMethod<any>;
remoteCustom?: boolean;
}

View File

@ -23,8 +23,6 @@ import 'vform3-builds/dist/designer.style.css';
import { setupVXETable } from '/@/hooks/setupVXETableHook';
// 自定义字典组件
import sysDict from '/@/components/sysDict/sysDict.vue';
// AI组件
import ElementPlusX from 'vue-element-plus-x';
// 关闭自动打印
import { disAutoConnect } from 'vue-plugin-hiprint';
disAutoConnect();
@ -36,4 +34,4 @@ other.elSvg(app);
// 注册全局字典组件
app.component('GSysDict', sysDict);
app.use(pinia).use(i18n).use(router).use(ElementPlus).use(setupVXETable).use(VueGridLayout).use(VForm3).use(VueSignaturePad).use(vue3TreeOrg).use(ElementPlusX).mount('#app');
app.use(pinia).use(i18n).use(router).use(ElementPlus).use(setupVXETable).use(VueGridLayout).use(VForm3).use(VueSignaturePad).use(vue3TreeOrg).mount('#app');

View File

@ -100,6 +100,7 @@ declare interface ThemeConfigState {
icp: string; // Icp备案号
icpUrl: string; // Icp地址
version?: string; // 版本号
carouselFiles?: any[]; // 轮播图集合
secondVer?: boolean; // 是否开启二级验证
captcha?: boolean; // 是否开启验证码
forceChangePassword?: boolean; // 是否开启强制修改密码

View File

@ -42,6 +42,8 @@ export async function loadSysInfo(tenantid: number) {
themeConfig.value.copyright = data.copyright;
// 版本号
themeConfig.value.version = data.version;
// 轮播图
themeConfig.value.carouselFiles = data.carouselFiles;
// 全局主题
themeConfig.value.primary = data.themeColor;
// 布局切换

View File

@ -2,8 +2,7 @@
<div style="flex: 1; background: #f3f4f6; overflow: auto">
<el-container>
<!-- 侧边栏 -->
<el-aside v-show="!isFold" :class="isFold ? 'sidebar-fold' : 'expand-sidebar'"
style="background: #f3f4f6; border-right: 1px solid #f0f0f0; display: flex; flex-direction: column">
<el-aside v-show="!isFold" :class="isFold ? 'sidebar-fold' : 'expand-sidebar'" style="background: #f3f4f6; border-right: 1px solid #f0f0f0; display: flex; flex-direction: column">
<div class="chat-action">
<el-tooltip :content="$t('message.chat.newChat')" placement="top">
<div class="chat-action-item" @click.stop="handleNewChat">
@ -16,23 +15,28 @@
</div>
</el-tooltip>
</div>
<div
style="display: flex; flex-direction: column; align-items: center; padding-bottom: 8px; padding-top: 0px; margin-top: 0px">
<el-avatar :size="60" style="background-color: #f3f4f6" src="/chat.png" fit="fill"
class="avatar-with-shadow" />
<div style="margin-top: 10px; font-weight: bold; font-size: 18px; color: #333">{{
$t('message.chat.title') }}</div>
<div style="display: flex; flex-direction: column; align-items: center; padding-bottom: 8px; padding-top: 0px; margin-top: 0px">
<el-avatar :size="60" style="background-color: #f3f4f6" src="/chat.png" fit="fill" class="avatar-with-shadow" />
<div style="margin-top: 10px; font-weight: bold; font-size: 18px; color: #333">{{ $t('message.chat.title') }}</div>
</div>
<div style="flex: 1; overflow-y: auto; width: 100%">
<div style="display: flex; flex-direction: column; gap: 12px; height: 100%">
<Conversations style="border-radius: 0%; width: 255px" v-model:active="activeHistoryKey"
:items="sideBarHistoryList" row-key="key" :show-tooltip="true" showToTopBtn
:label-max-width="210" :load-more="loadMoreHistoryItems"
:load-more-loading="isHistoryListLoading" showBuiltInMenu @change="handleChange">
<Conversations
style="border-radius: 0%; width: 255px"
v-model:active="activeHistoryKey"
:items="sideBarHistoryList"
row-key="key"
:show-tooltip="true"
showToTopBtn
:label-max-width="210"
:load-more="loadMoreHistoryItems"
:load-more-loading="isHistoryListLoading"
showBuiltInMenu
@change="handleChange"
>
<template #menu="{ item }">
<div class="menu-buttons">
<el-button v-for="menuItem in actionMenuItems" :key="menuItem.key" link
size="default" @click="handleMenuClick(menuItem.key, item)">
<el-button v-for="menuItem in actionMenuItems" :key="menuItem.key" link size="default" @click="handleMenuClick(menuItem.key, item)">
<el-icon v-if="menuItem.icon">
<component :is="menuItem.icon" />
</el-icon>
@ -47,7 +51,8 @@
<!-- 主体内容 -->
<el-container>
<el-main style="
<el-main
style="
flex: 1;
width: 100%;
height: 100%;
@ -60,12 +65,12 @@
align-items: center;
justify-content: flex-start;
background: #fff;
">
"
>
<div class="main_action_toolbar" style="position: absolute; top: 0px; width: 500px; left: 20px">
<div v-if="isFold" class="chat-action-item main_action_item">
<el-tooltip :content="$t('message.chat.expandChat')" placement="top">
<Expand style="width: 1.5em; height: 1.5em; color: #333"
@click.stop="handleExpandChat" />
<Expand style="width: 1.5em; height: 1.5em; color: #333" @click.stop="handleExpandChat" />
</el-tooltip>
</div>
<div v-if="isFold" class="chat-action-item main_action_item" @click="handleNewChat">
@ -83,12 +88,10 @@
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in modellList.models" :key="item.modelName"
@click="handleChangeModel(item)">
<el-dropdown-item v-for="item in modellList.models" :key="item.modelName" @click="handleChangeModel(item)">
<div class="model-item">
<span>{{ item.providerName }}/{{ item.modelName }}</span>
<el-icon v-if="item.modelName == modellList.currentModel"
style="color: #409eff">
<el-icon v-if="item.modelName == modellList.currentModel" style="color: #409eff">
<Select />
</el-icon>
</div>
@ -99,46 +102,35 @@
</div>
</div>
<div v-if="isNew" class="new_chat_title">
<div style="margin-top: 60px; font-size: 36px; font-weight: bold; letter-spacing: 2px">Hello, {{
userName }}
</div>
<div
style="margin-top: 20px; width: 100%; text-align: center; color: #999; font-size: 16px; font-weight: bold; letter-spacing: 2px">
<div style="margin-top: 60px; font-size: 36px; font-weight: bold; letter-spacing: 2px">Hello, {{ userName }}</div>
<div style="margin-top: 20px; width: 100%; text-align: center; color: #999; font-size: 16px; font-weight: bold; letter-spacing: 2px">
{{ $t('message.chat.subTitle') }}
</div>
</div>
<div v-else class="chat_content">
<BubbleList class="chat_content_list" ref="chatRef" :list="chatList" maxHeight="100%"
style="padding: 0 60px 100px 60px" @complete="handleBubbleComplete"
:triggerIndices="triggerIndices">
<BubbleList class="chat_content_list" ref="chatRef" :list="chatList" maxHeight="100%" style="padding: 0 60px 100px 60px" @complete="handleBubbleComplete" :triggerIndices="triggerIndices">
<template #content="{ item }">
<Typewriter v-if="chatList.length > 0 && chatList[chatList.length - 1].key === item.key && isSenderLoading" :content="item.content" :is-markdown="true" :typing="true" @finish="handleTypeingComplete"/>
<XMarkdown
v-else
:markdown="item.content"
default-theme-mode="light"
class="markdown-body"
<Typewriter
v-if="chatList.length > 0 && chatList[chatList.length - 1].key === item.key && isSenderLoading"
:content="item.content"
:is-markdown="true"
:typing="true"
@finish="handleTypeingComplete"
/>
<XMarkdown v-else :markdown="item.content" default-theme-mode="light" class="markdown-body" />
</template>
<template #header="{ item }">
<div v-if="item.role == 'assistant' && deepThinkingVisible && item.key == chatList[chatList.length - 1].key"
class="header-wrapper">
<Thinking max-width="100%" buttonWidth="250px" autoCollapse
:content="deepThinkingMessage" :status="deepThinkingStatus"
backgroundColor="#fff9e6" color="#000">
<div v-if="item.role == 'assistant' && deepThinkingVisible && item.key == chatList[chatList.length - 1].key" class="header-wrapper">
<Thinking max-width="100%" buttonWidth="250px" autoCollapse :content="deepThinkingMessage" :status="deepThinkingStatus" backgroundColor="#fff9e6" color="#000">
<template #label="{ status }">
<span v-if="status === 'start'">{{ $t('message.chat.startThinking')
}}</span>
<span v-else-if="status === 'thinking'">{{ $t('message.chat.thinking')
}}</span>
<span v-else-if="status === 'end'">{{ $t('message.chat.thinkingDone')
}}</span>
<span v-else-if="status === 'error'">{{ $t('message.chat.thinkingFailed')
}}</span>
<span v-if="status === 'start'">{{ $t('message.chat.startThinking') }}</span>
<span v-else-if="status === 'thinking'">{{ $t('message.chat.thinking') }}</span>
<span v-else-if="status === 'end'">{{ $t('message.chat.thinkingDone') }}</span>
<span v-else-if="status === 'error'">{{ $t('message.chat.thinkingFailed') }}</span>
</template>
<template #content="{ content }">
<span>
<XMarkdown :markdown="content" default-theme-mode="light" class="vp-raw" />
<XMarkdown :markdown="content" default-theme-mode="light" class="vp-raw" />
</span>
</template>
</Thinking>
@ -146,15 +138,12 @@
</template>
<template #footer="{ item }">
<div v-if="item.role == 'assistant'" class="footer-container">
<el-button type="info" text :icon="CopyDocument" size="small"
@click="handleCopy(item)" />
<el-button type="info" text :icon="CopyDocument" size="small" @click="handleCopy(item)" />
<el-button type="info" text size="small">
<el-icon v-if="!isPlaying || playAudioKey != item.key"
@click="handlePlay(item)">
<el-icon v-if="!isPlaying || playAudioKey != item.key" @click="handlePlay(item)">
<VideoPlay />
</el-icon>
<el-icon v-if="isPlaying && playAudioKey == item.key"
@click="handlePause(item)">
<el-icon v-if="isPlaying && playAudioKey == item.key" @click="handlePause(item)">
<VideoPause />
</el-icon>
</el-button>
@ -163,9 +152,18 @@
</BubbleList>
</div>
<div :class="isNew ? 'chat_new_input_style' : 'chat_edit_input_style'">
<sender ref="senderRef" variant="updown" clearable allow-speech :loading="isSenderLoading"
:read-only="isSenderLoading" :auto-size="{ minRows: 1, maxRows: 5 }" v-model="senderInput"
@submit="handleSend" :placeholder="$t('message.chat.inputPlaceholder')">
<sender
ref="senderRef"
variant="updown"
clearable
allow-speech
:loading="isSenderLoading"
:read-only="isSenderLoading"
:auto-size="{ minRows: 1, maxRows: 5 }"
v-model="senderInput"
@submit="handleSend"
:placeholder="$t('message.chat.inputPlaceholder')"
>
<template #prefix>
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap">
<el-button round plain>
@ -174,9 +172,11 @@
</el-icon>
</el-button>
<div :class="{ isDeepThinking }"
<div
:class="{ isDeepThinking }"
style="display: flex; align-items: center; gap: 4px; padding: 4px 12px; border: 1px solid silver; border-radius: 15px; cursor: pointer; font-size: 12px"
@click="handleDeepThinking">
@click="handleDeepThinking"
>
<el-icon>
<ElementPlus />
</el-icon>
@ -197,12 +197,10 @@
<div class="ai-rename-icon ai-rename-drag" @mousedown="startDrag">
<el-avatar :size="48" src="/chat.png" />
</div>
<el-input v-model="renameInput" @keyup.enter="confirmRename" ref="renameInputRef"
class="ai-rename-input" />
<el-input v-model="renameInput" @keyup.enter="confirmRename" ref="renameInputRef" class="ai-rename-input" />
</div>
<div class="ai-dialog-footer">
<el-button @click="cancelRename" class="ai-btn-cancel">{{ $t('message.chat.cancel')
}}</el-button>
<el-button @click="cancelRename" class="ai-btn-cancel">{{ $t('message.chat.cancel') }}</el-button>
<el-button type="primary" @click="confirmRename" class="ai-btn-confirm">
{{ $t('message.chat.confirm') }}
</el-button>
@ -222,12 +220,10 @@ import { useI18n } from 'vue-i18n';
import { franc } from 'franc';
import { v4 as uuidv4 } from 'uuid';
import { BubbleList, Conversations, Sender, Thinking, Typewriter, XMarkdown } from 'vue-element-plus-x';
import type { BubbleListItemProps, BubbleListProps } from 'vue-element-plus-x/types/components/BubbleList/types';
import type { ConversationItem } from 'vue-element-plus-x/types/components/Conversations/types';
import type { TypewriterInstance } from 'vue-element-plus-x/types/components/Typewriter/types';
// import 'vue-element-plus-x/styles/prism-coy.min.css'
// import 'vue-element-plus-x/styles/prism.min.css'
import { getAPI } from '/@/utils/axios-utils';
import { LLMChatApi } from '/@/api-services/api';
@ -311,13 +307,13 @@ const initSSEConnection = () => {
monitorSSEConnectionHandler = setInterval(() => {
isSSEConnectionClosed = Date.now() - lastSseConnectionTime > SSE_CONNECTION_TIMEOUT;
if (isSSEConnectionClosed) {
console.log("SSE connection timed out, reconnecting");
console.log('SSE connection timed out, reconnecting');
try {
initSSEConnectionCore();
} catch (err) {
console.log("SSE connection timed out, reconnecting failed", err);
console.log('SSE connection timed out, reconnecting failed', err);
}
}
}
}, SSE_CONNECTION_TIMEOUT);
};
@ -327,7 +323,7 @@ const checkSSEConnectionStatus = () => {
return false;
}
return true;
}
};
// sse
const initSSEConnectionCore = () => {
closeSSEConnection();
@ -378,12 +374,10 @@ const initSSEConnectionCore = () => {
lastSseConnectionTime = Date.now();
});
eventSource.onerror = () => {
console.log('SSE connection error');
};
eventSource.onopen = (event) => {
console.log('SSE connection opened:', event);
};
@ -401,10 +395,10 @@ const handleBubbleComplete = (instance: TypewriterInstance, index: number) => {
chatRef?.value?.scrollToBottom();
};
const handleTypeingComplete = ()=>{
const handleTypeingComplete = () => {
isSenderLoading.value = false;
chatRef?.value?.scrollToBottom();
}
};
//#endregion sse
const handleSend = async () => {
@ -688,8 +682,6 @@ const loadMoreHistoryItems = async () => {
isHistoryListLoading.value = false;
};
const handleMenuClick = (menuKey: string, item: any) => {
switch (menuKey) {
case 'rename':
@ -1129,9 +1121,9 @@ onBeforeUnmount(() => {
}
.vp-raw {
background-color: #f0f0f0;
padding: 2px 20px;
border-radius: 4px;
line-height: 2em;
padding: 2px 20px;
border-radius: 4px;
line-height: 2em;
}
.markdown-body {
font-size: 16px !important;

View File

@ -10,15 +10,22 @@
</div>
</div>
<el-carousel height="500px" class="login-carousel">
<el-carousel-item>
<img :src="loginIconTwo" class="login-icon-group-icon" />
</el-carousel-item>
<el-carousel-item>
<img :src="loginIconTwo1" class="login-icon-group-icon" />
</el-carousel-item>
<el-carousel-item>
<img :src="loginIconTwo2" class="login-icon-group-icon" />
</el-carousel-item>
<template v-if="themeConfig.carouselFiles == undefined || themeConfig.carouselFiles.length < 1">
<el-carousel-item>
<img :src="loginIconTwo" class="login-icon-group-icon" />
</el-carousel-item>
<el-carousel-item>
<img :src="loginIconTwo1" class="login-icon-group-icon" />
</el-carousel-item>
<el-carousel-item>
<img :src="loginIconTwo2" class="login-icon-group-icon" />
</el-carousel-item>
</template>
<template v-else>
<el-carousel-item v-for="(item, key) in themeConfig.carouselFiles" :key="key">
<img :src="item.url" class="login-icon-group-icon" />
</el-carousel-item>
</template>
</el-carousel>
</div>
<div class="login-right flex">
@ -95,6 +102,7 @@ import { Local } from '/@/utils/storage';
//
import { languageList, getCountryCode } from '/@/i18n';
import '/node_modules/flag-icons/css/flag-icons.min.css';
import { template } from 'xe-utils';
//
const Account = defineAsyncComponent(() => import('/@/views/login/component/account.vue'));

View File

@ -9,7 +9,7 @@
<template #label>
<el-icon><ele-School /></el-icon>
</template>
<el-upload ref="uploadRef" class="avatar-uploader" :showFileList="false" :autoUpload="false" accept=".jpg,.png,.svg" action :limit="1" :onChange="handleUploadChange">
<el-upload ref="uploadRef" class="avatar-uploader" :showFileList="false" :autoUpload="false" accept=".jpg,.png,.svg" action :limit="1" :onChange="handleUploadLogoChange">
<img v-if="state.sysInfo.logo" :src="state.sysInfo.logo" class="avatar" />
<SvgIcon v-else class="avatar-uploader-icon" name="ele-Plus" :size="28" />
</el-upload>
@ -152,13 +152,21 @@
<template #label>
<el-icon><ele-Picture /></el-icon>
</template>
<el-upload :file-list="state.carouselFileList" list-type="picture-card" :http-request="uploadCarouselFile" :on-preview="previewCarouselFile" :before-remove="beforeRemoveCarouselFile">
<el-upload
:file-list="state.carouselFileList"
list-type="picture-card"
:autoUpload="false"
accept=".jpg,.png,.svg"
:onChange="handleCarouselChange"
:on-preview="handleCarouselPreview"
:before-remove="handleCarouselRemove"
>
<el-icon><ele-Plus /></el-icon>
</el-upload>
</el-descriptions-item>
<template #extra>
<el-button type="primary" icon="ele-SuccessFilled" @click="saveSysInfo">保存</el-button>
<el-button type="primary" icon="ele-SuccessFilled" @click="saveSysInfo">保存配置</el-button>
</template>
</el-descriptions>
</el-card>
@ -182,63 +190,67 @@
</el-row>
</div>
</el-dialog>
<el-dialog v-model="state.dialogImagePreviewVisible">
<img w-full :src="state.dialogImagePreviewUrl" alt="" />
</el-dialog>
</div>
</template>
<script setup lang="ts" name="sysInfoSetting">
import { nextTick, onMounted, reactive, ref } from 'vue';
import { ElMessage, ElMessageBox, UploadInstance } from 'element-plus';
import { fileToBase64 } from '/@/utils/base64Conver';
import { ElMessage, UploadFile, UploadFiles, UploadInstance } from 'element-plus';
import { loadSysInfo } from '/@/utils/sysInfo';
import chineseColors from '/@/layout/navBars/topBar/colors.json';
import { getAPI } from '/@/utils/axios-utils';
import { SysFileApi, SysInfoInput, SysTenantApi } from '/@/api-services';
import { SysFileApi, SysTenantApi } from '/@/api-services';
const host = window.location.host;
const uploadRef = ref<UploadInstance>();
const state = reactive({
isLoading: false,
sysInfo: {} as SysInfoInput | any, //
sysInfo: {} as any, //
logoFile: undefined as any, // logo
dialogChineseColorVisible: false, //
colorName: '飞燕草蓝', //
dialogImagePreviewVisible: false, //
dialogImagePreviewUrl: '', //
isDelete: false, //
carouselFileList: [] as any, //
carouselFileList: [] as any, //
});
//
onMounted(async () => {
await loadSysInfoData();
state.isLoading = true;
state.sysInfo = await getAPI(SysTenantApi)
.apiSysTenantSysInfoTenantIdGet(0)
.then((res) => res.data.result ?? []);
if (state.sysInfo.carouselFiles) state.carouselFileList = state.sysInfo.carouselFiles;
state.isLoading = false;
});
// onChange
const handleUploadChange = (file: any) => {
uploadRef.value!.clearFiles();
state.logoFile = file;
state.sysInfo.logo = URL.createObjectURL(state.logoFile.raw); // logo
};
//
const saveSysInfo = async () => {
// base64
if (state.logoFile) {
state.sysInfo.logoBase64 = (await fileToBase64(state.logoFile.raw)) as string;
state.sysInfo.logoFileName = state.logoFile.raw.name;
}
try {
state.isLoading = true;
if (state.logoFile != undefined && state.logoFile.raw != undefined) {
var res = await getAPI(SysFileApi).apiSysFileUploadLogoTenantIdPostForm(state.sysInfo.tenantId, state.logoFile.raw);
state.sysInfo.logo = res.data.result?.url;
}
if (state.carouselFileList != undefined && state.carouselFileList.length > 0) {
//
state.sysInfo.carouselFileIds = state.carouselFileList.filter((file: any) => file.raw == undefined && file.id != undefined).map((file: any) => file.id);
//
var carouselFiles = state.carouselFileList.filter((file: any) => file.raw != undefined).map((file: any) => file.raw);
if (carouselFiles.length > 0) {
var sysFileIds = await getAPI(SysFileApi)
.apiSysFileUploadCarouselTenantIdPostForm(state.sysInfo.tenantId, carouselFiles)
.then((res) => res.data.result?.map((file: any) => file.id) ?? []);
state.sysInfo.carouselFileIds.push(...sysFileIds);
}
console.log(state.sysInfo.carouselFileIds);
}
await getAPI(SysTenantApi).apiSysTenantSaveSysInfoPost(state.sysInfo);
state.logoFile = undefined;
ElMessage.success('保存成功');
//
state.logoFile = undefined;
//
await loadSysInfo(state.sysInfo.tenantId);
} finally {
@ -248,65 +260,27 @@ const saveSysInfo = async () => {
}
};
//
const loadSysInfoData = async () => {
try {
state.isLoading = true;
const res = await getAPI(SysTenantApi).apiSysTenantSysInfoTenantIdGet(0);
if (res.data!.type !== 'success') return;
state.sysInfo = res.data.result;
if (state.sysInfo.carouselFiles) state.carouselFileList = state.sysInfo.carouselFiles;
} finally {
nextTick(() => {
state.isLoading = false;
});
}
// logo
const handleUploadLogoChange = (file: any) => {
uploadRef.value!.clearFiles();
state.logoFile = file;
state.sysInfo.logo = URL.createObjectURL(state.logoFile.raw); // logo
};
// file
// const onlineImageToFile = async (imageUrl: string | URL | Request, fileName: string) => {
// try {
// const response = await fetch(imageUrl);
// const blob = await response.blob();
// const file = new File([blob], fileName, { type: blob.type });
// return file;
// } catch (error) {
// return null;
// }
// };
//
const uploadCarouselFile = async (e: any) => {
await getAPI(SysTenantApi).apiSysTenantUploadCarouselFilePostForm(e.file);
const handleCarouselChange = async (file: UploadFile, files: UploadFiles) => {
state.carouselFileList = files;
};
//
const beforeRemoveCarouselFile = (file: any, fileList: any) => {
const result = new Promise((resolve, reject) => {
ElMessageBox.confirm(`确定删除此轮播图?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
state.isDelete = true;
let index = fileList.indexOf(file);
await getAPI(SysFileApi).apiSysFileDeletePost(fileList[index]);
fileList.splice(index, 1);
state.carouselFileList.splice(index, 1);
})
.catch(() => {
reject(false);
});
});
return result;
const handleCarouselRemove = (file: UploadFile) => {
let index = state.carouselFileList.indexOf(file);
state.carouselFileList.splice(index, 1);
};
//
const previewCarouselFile = (file: any) => {
state.dialogImagePreviewUrl = file.url!;
state.dialogImagePreviewVisible = true;
const handleCarouselPreview = (file: UploadFile) => {
window.open(file.url);
};
//