Merge branch 'ai-agent' into ai-hub
# Conflicts: # Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// 创建AI应用输入
|
||||
/// </summary>
|
||||
public class AiAppCreateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "应用名称不能为空")]
|
||||
[StringLength(100, ErrorMessage = "应用名称不能超过100个字符")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用终结点
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "应用终结点不能为空")]
|
||||
[StringLength(500, ErrorMessage = "应用终结点不能超过500个字符")]
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 额外URL
|
||||
/// </summary>
|
||||
[StringLength(500, ErrorMessage = "额外URL不能超过500个字符")]
|
||||
public string? ExtraUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用Key
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "应用Key不能为空")]
|
||||
[StringLength(500, ErrorMessage = "应用Key不能超过500个字符")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue, ErrorMessage = "排序必须大于等于0")]
|
||||
public int OrderNum { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// AI应用DTO
|
||||
/// </summary>
|
||||
public class AiAppDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用ID
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用终结点
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 额外URL
|
||||
/// </summary>
|
||||
public string? ExtraUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用Key
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
public int OrderNum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreationTime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// 获取AI应用列表输入
|
||||
/// </summary>
|
||||
public class AiAppGetListInput : PagedAllResultRequestDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 搜索关键词(搜索应用名称)
|
||||
/// </summary>
|
||||
public string? SearchKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// 更新AI应用输入
|
||||
/// </summary>
|
||||
public class AiAppUpdateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "应用ID不能为空")]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "应用名称不能为空")]
|
||||
[StringLength(100, ErrorMessage = "应用名称不能超过100个字符")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用终结点
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "应用终结点不能为空")]
|
||||
[StringLength(500, ErrorMessage = "应用终结点不能超过500个字符")]
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 额外URL
|
||||
/// </summary>
|
||||
[StringLength(500, ErrorMessage = "额外URL不能超过500个字符")]
|
||||
public string? ExtraUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用Key
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "应用Key不能为空")]
|
||||
[StringLength(500, ErrorMessage = "应用Key不能超过500个字符")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue, ErrorMessage = "排序必须大于等于0")]
|
||||
public int OrderNum { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// 创建AI模型输入
|
||||
/// </summary>
|
||||
public class AiModelCreateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "处理名不能为空")]
|
||||
[StringLength(100, ErrorMessage = "处理名不能超过100个字符")]
|
||||
public string HandlerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "模型ID不能为空")]
|
||||
[StringLength(200, ErrorMessage = "模型ID不能超过200个字符")]
|
||||
public string ModelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "模型名称不能为空")]
|
||||
[StringLength(200, ErrorMessage = "模型名称不能超过200个字符")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型描述
|
||||
/// </summary>
|
||||
[StringLength(1000, ErrorMessage = "模型描述不能超过1000个字符")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue, ErrorMessage = "排序必须大于等于0")]
|
||||
public int OrderNum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AI应用ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "AI应用ID不能为空")]
|
||||
public Guid AiAppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 额外信息
|
||||
/// </summary>
|
||||
[StringLength(2000, ErrorMessage = "额外信息不能超过2000个字符")]
|
||||
public string? ExtraInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型类型
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "模型类型不能为空")]
|
||||
public ModelTypeEnum ModelType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型API类型
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "模型API类型不能为空")]
|
||||
public ModelApiTypeEnum ModelApiType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型倍率
|
||||
/// </summary>
|
||||
[Range(0.01, double.MaxValue, ErrorMessage = "模型倍率必须大于0")]
|
||||
public decimal Multiplier { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 模型显示倍率
|
||||
/// </summary>
|
||||
[Range(0.01, double.MaxValue, ErrorMessage = "模型显示倍率必须大于0")]
|
||||
public decimal MultiplierShow { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 供应商分组名称
|
||||
/// </summary>
|
||||
[StringLength(100, ErrorMessage = "供应商分组名称不能超过100个字符")]
|
||||
public string? ProviderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型图标URL
|
||||
/// </summary>
|
||||
[StringLength(500, ErrorMessage = "模型图标URL不能超过500个字符")]
|
||||
public string? IconUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为尊享模型
|
||||
/// </summary>
|
||||
public bool IsPremium { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// AI模型DTO
|
||||
/// </summary>
|
||||
public class AiModelDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 模型ID
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理名
|
||||
/// </summary>
|
||||
public string HandlerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型ID
|
||||
/// </summary>
|
||||
public string ModelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型描述
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
public int OrderNum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AI应用ID
|
||||
/// </summary>
|
||||
public Guid AiAppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 额外信息
|
||||
/// </summary>
|
||||
public string? ExtraInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型类型
|
||||
/// </summary>
|
||||
public ModelTypeEnum ModelType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型API类型
|
||||
/// </summary>
|
||||
public ModelApiTypeEnum ModelApiType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型倍率
|
||||
/// </summary>
|
||||
public decimal Multiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型显示倍率
|
||||
/// </summary>
|
||||
public decimal MultiplierShow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 供应商分组名称
|
||||
/// </summary>
|
||||
public string? ProviderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型图标URL
|
||||
/// </summary>
|
||||
public string? IconUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为尊享模型
|
||||
/// </summary>
|
||||
public bool IsPremium { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// 获取AI模型列表输入
|
||||
/// </summary>
|
||||
public class AiModelGetListInput : PagedAllResultRequestDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 搜索关键词(搜索模型名称、模型ID)
|
||||
/// </summary>
|
||||
public string? SearchKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AI应用ID筛选
|
||||
/// </summary>
|
||||
public Guid? AiAppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否只显示尊享模型
|
||||
/// </summary>
|
||||
public bool? IsPremiumOnly { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// 更新AI模型输入
|
||||
/// </summary>
|
||||
public class AiModelUpdateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 模型ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "模型ID不能为空")]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理名
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "处理名不能为空")]
|
||||
[StringLength(100, ErrorMessage = "处理名不能超过100个字符")]
|
||||
public string HandlerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "模型ID不能为空")]
|
||||
[StringLength(200, ErrorMessage = "模型ID不能超过200个字符")]
|
||||
public string ModelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型名称
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "模型名称不能为空")]
|
||||
[StringLength(200, ErrorMessage = "模型名称不能超过200个字符")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型描述
|
||||
/// </summary>
|
||||
[StringLength(1000, ErrorMessage = "模型描述不能超过1000个字符")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue, ErrorMessage = "排序必须大于等于0")]
|
||||
public int OrderNum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AI应用ID
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "AI应用ID不能为空")]
|
||||
public Guid AiAppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 额外信息
|
||||
/// </summary>
|
||||
[StringLength(2000, ErrorMessage = "额外信息不能超过2000个字符")]
|
||||
public string? ExtraInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型类型
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "模型类型不能为空")]
|
||||
public ModelTypeEnum ModelType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型API类型
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "模型API类型不能为空")]
|
||||
public ModelApiTypeEnum ModelApiType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型倍率
|
||||
/// </summary>
|
||||
[Range(0.01, double.MaxValue, ErrorMessage = "模型倍率必须大于0")]
|
||||
public decimal Multiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型显示倍率
|
||||
/// </summary>
|
||||
[Range(0.01, double.MaxValue, ErrorMessage = "模型显示倍率必须大于0")]
|
||||
public decimal MultiplierShow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 供应商分组名称
|
||||
/// </summary>
|
||||
[StringLength(100, ErrorMessage = "供应商分组名称不能超过100个字符")]
|
||||
public string? ProviderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型图标URL
|
||||
/// </summary>
|
||||
[StringLength(500, ErrorMessage = "模型图标URL不能超过500个字符")]
|
||||
public string? IconUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为尊享模型
|
||||
/// </summary>
|
||||
public bool IsPremium { get; set; }
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public class AgentSendInput
|
||||
/// <summary>
|
||||
/// api密钥Id
|
||||
/// </summary>
|
||||
public string Token { get; set; }
|
||||
public Guid TokenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型id
|
||||
|
||||
@@ -5,6 +5,11 @@ namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||
/// </summary>
|
||||
public class ImageGenerationInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 密钥id
|
||||
/// </summary>
|
||||
public Guid? TokenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提示词
|
||||
/// </summary>
|
||||
@@ -16,7 +21,7 @@ public class ImageGenerationInput
|
||||
public string ModelId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 参考图Base64列表(可选,包含前缀如 data:image/png;base64,...)
|
||||
/// 参考图PrefixBase64列表(可选,包含前缀如 data:image/png;base64,...)
|
||||
/// </summary>
|
||||
public List<string>? ReferenceImagesBase64 { get; set; }
|
||||
public List<string>? ReferenceImagesPrefixBase64 { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 图片任务分页查询输入
|
||||
/// </summary>
|
||||
public class ImageTaskPageInput
|
||||
public class ImageMyTaskPageInput: PagedAllResultRequestDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 页码(从1开始)
|
||||
/// 提示词
|
||||
/// </summary>
|
||||
public int PageIndex { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页数量
|
||||
/// </summary>
|
||||
public int PageSize { get; set; } = 10;
|
||||
|
||||
public string? Prompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务状态筛选(可选)
|
||||
/// </summary>
|
||||
public TaskStatusEnum? TaskStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发布状态
|
||||
/// </summary>
|
||||
public PublishStatusEnum? PublishStatus { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 图片任务分页查询输入
|
||||
/// </summary>
|
||||
public class ImagePlazaPageInput: PagedAllResultRequestDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 分类
|
||||
/// </summary>
|
||||
public string? Categories { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提示词
|
||||
/// </summary>
|
||||
public string? Prompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务状态筛选(可选)
|
||||
/// </summary>
|
||||
public TaskStatusEnum? TaskStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string? UserName{ get; set; }
|
||||
}
|
||||
@@ -18,19 +18,9 @@ public class ImageTaskOutput
|
||||
public string Prompt { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 参考图Base64列表
|
||||
/// 是否匿名
|
||||
/// </summary>
|
||||
public List<string>? ReferenceImagesBase64 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 参考图URL列表
|
||||
/// </summary>
|
||||
public List<string>? ReferenceImagesUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生成图片Base64(包含前缀)
|
||||
/// </summary>
|
||||
public string? StoreBase64 { get; set; }
|
||||
public bool IsAnonymous { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生成图片URL
|
||||
@@ -42,8 +32,35 @@ public class ImageTaskOutput
|
||||
/// </summary>
|
||||
public TaskStatusEnum TaskStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发布状态
|
||||
/// </summary>
|
||||
public PublishStatusEnum PublishStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类标签
|
||||
/// </summary>
|
||||
[SqlSugar.SugarColumn( IsJson = true)]
|
||||
public List<string> Categories { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
public string? ErrorInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户名称
|
||||
/// </summary>
|
||||
public string? UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户名称Id
|
||||
/// </summary>
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 发布图片输入
|
||||
/// </summary>
|
||||
public class PublishImageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否匿名
|
||||
/// </summary>
|
||||
public bool IsAnonymous { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 任务ID
|
||||
/// </summary>
|
||||
public Guid TaskId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类标签
|
||||
/// </summary>
|
||||
public List<string> Categories { get; set; } = new();
|
||||
}
|
||||
@@ -6,13 +6,7 @@ public class ModelGetListOutput
|
||||
/// 模型ID
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型分类
|
||||
/// </summary>
|
||||
public string Category { get; set; }
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 模型id
|
||||
/// </summary>
|
||||
@@ -28,36 +22,6 @@ public class ModelGetListOutput
|
||||
/// </summary>
|
||||
public string? ModelDescribe { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型价格
|
||||
/// </summary>
|
||||
public double ModelPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型类型
|
||||
/// </summary>
|
||||
public string ModelType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型展示状态
|
||||
/// </summary>
|
||||
public string ModelShow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 系统提示
|
||||
/// </summary>
|
||||
public string SystemPrompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// API 主机地址
|
||||
/// </summary>
|
||||
public string ApiHost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// API 密钥
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道商管理服务接口
|
||||
/// </summary>
|
||||
public interface IChannelService
|
||||
{
|
||||
#region AI应用管理
|
||||
|
||||
/// <summary>
|
||||
/// 获取AI应用列表
|
||||
/// </summary>
|
||||
/// <param name="input">查询参数</param>
|
||||
/// <returns>分页应用列表</returns>
|
||||
Task<PagedResultDto<AiAppDto>> GetAppListAsync(AiAppGetListInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取AI应用
|
||||
/// </summary>
|
||||
/// <param name="id">应用ID</param>
|
||||
/// <returns>应用详情</returns>
|
||||
Task<AiAppDto> GetAppByIdAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// 创建AI应用
|
||||
/// </summary>
|
||||
/// <param name="input">创建输入</param>
|
||||
/// <returns>创建的应用</returns>
|
||||
Task<AiAppDto> CreateAppAsync(AiAppCreateInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新AI应用
|
||||
/// </summary>
|
||||
/// <param name="input">更新输入</param>
|
||||
/// <returns>更新后的应用</returns>
|
||||
Task<AiAppDto> UpdateAppAsync(AiAppUpdateInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除AI应用
|
||||
/// </summary>
|
||||
/// <param name="id">应用ID</param>
|
||||
Task DeleteAppAsync(Guid id);
|
||||
|
||||
#endregion
|
||||
|
||||
#region AI模型管理
|
||||
|
||||
/// <summary>
|
||||
/// 获取AI模型列表
|
||||
/// </summary>
|
||||
/// <param name="input">查询参数</param>
|
||||
/// <returns>分页模型列表</returns>
|
||||
Task<PagedResultDto<AiModelDto>> GetModelListAsync(AiModelGetListInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取AI模型
|
||||
/// </summary>
|
||||
/// <param name="id">模型ID</param>
|
||||
/// <returns>模型详情</returns>
|
||||
Task<AiModelDto> GetModelByIdAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// 创建AI模型
|
||||
/// </summary>
|
||||
/// <param name="input">创建输入</param>
|
||||
/// <returns>创建的模型</returns>
|
||||
Task<AiModelDto> CreateModelAsync(AiModelCreateInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新AI模型
|
||||
/// </summary>
|
||||
/// <param name="input">更新输入</param>
|
||||
/// <returns>更新后的模型</returns>
|
||||
Task<AiModelDto> UpdateModelAsync(AiModelUpdateInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除AI模型(软删除)
|
||||
/// </summary>
|
||||
/// <param name="id">模型ID</param>
|
||||
Task DeleteModelAsync(Guid id);
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -30,32 +30,94 @@ public class ImageGenerationJob : AsyncBackgroundJob<ImageGenerationJobArgs>, IT
|
||||
|
||||
public override async Task ExecuteAsync(ImageGenerationJobArgs args)
|
||||
{
|
||||
_logger.LogInformation("开始执行图片生成任务,TaskId: {TaskId}, ModelId: {ModelId}, UserId: {UserId}",
|
||||
args.TaskId, args.ModelId, args.UserId);
|
||||
var task = await _imageStoreTaskRepository.GetFirstAsync(x => x.Id == args.TaskId);
|
||||
if (task is null)
|
||||
{
|
||||
throw new UserFriendlyException($"{args.TaskId} 图片生成任务不存在");
|
||||
}
|
||||
|
||||
_logger.LogInformation("开始执行图片生成任务,TaskId: {TaskId}, ModelId: {ModelId}, UserId: {UserId}",
|
||||
task.Id, task.ModelId, task.UserId);
|
||||
try
|
||||
{
|
||||
var request = JsonSerializer.Deserialize<JsonElement>(args.RequestJson);
|
||||
// 构建 Gemini API 请求对象
|
||||
var parts = new List<object>
|
||||
{
|
||||
new { text = task.Prompt }
|
||||
};
|
||||
|
||||
// 添加参考图(如果有)
|
||||
foreach (var prefixBase64 in task.ReferenceImagesPrefixBase64)
|
||||
{
|
||||
var (mimeType, base64Data) = ParsePrefixBase64(prefixBase64);
|
||||
parts.Add(new
|
||||
{
|
||||
inline_data = new
|
||||
{
|
||||
mime_type = mimeType,
|
||||
data = base64Data
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var requestObj = new
|
||||
{
|
||||
contents = new[]
|
||||
{
|
||||
new { role = "user", parts }
|
||||
}
|
||||
};
|
||||
|
||||
var request = JsonSerializer.Deserialize<JsonElement>(
|
||||
JsonSerializer.Serialize(requestObj));
|
||||
|
||||
//里面生成成功已经包含扣款了
|
||||
await _aiGateWayManager.GeminiGenerateContentImageForStatisticsAsync(
|
||||
args.TaskId,
|
||||
args.ModelId,
|
||||
task.Id,
|
||||
task.ModelId,
|
||||
request,
|
||||
args.UserId);
|
||||
task.UserId,
|
||||
tokenId: task.TokenId);
|
||||
|
||||
|
||||
_logger.LogInformation("图片生成任务完成,TaskId: {TaskId}", args.TaskId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "图片生成任务失败,TaskId: {TaskId}, Error: {Error}", args.TaskId, ex.Message);
|
||||
var error = $"图片任务失败,TaskId: {args.TaskId},错误信息: {ex.Message},错误堆栈:{ex.StackTrace}";
|
||||
_logger.LogError(ex, error);
|
||||
|
||||
// 更新任务状态为失败
|
||||
var task = await _imageStoreTaskRepository.GetFirstAsync(x => x.Id == args.TaskId);
|
||||
if (task != null)
|
||||
{
|
||||
task.TaskStatus = TaskStatusEnum.Fail;
|
||||
await _imageStoreTaskRepository.UpdateAsync(task);
|
||||
}
|
||||
task.TaskStatus = TaskStatusEnum.Fail;
|
||||
task.ErrorInfo = error;
|
||||
|
||||
await _imageStoreTaskRepository.UpdateAsync(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析带前缀的 Base64 字符串,提取 mimeType 和纯 base64 数据
|
||||
/// </summary>
|
||||
private static (string mimeType, string base64Data) ParsePrefixBase64(string prefixBase64)
|
||||
{
|
||||
// 默认值
|
||||
var mimeType = "image/png";
|
||||
var base64Data = prefixBase64;
|
||||
|
||||
if (prefixBase64.Contains(","))
|
||||
{
|
||||
var parts = prefixBase64.Split(',');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var header = parts[0];
|
||||
if (header.Contains(":") && header.Contains(";"))
|
||||
{
|
||||
mimeType = header.Split(':')[1].Split(';')[0];
|
||||
}
|
||||
|
||||
base64Data = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
return (mimeType, base64Data);
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,4 @@ public class ImageGenerationJobArgs
|
||||
/// 图片任务ID
|
||||
/// </summary>
|
||||
public Guid TaskId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型ID
|
||||
/// </summary>
|
||||
public string ModelId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 请求JSON字符串
|
||||
/// </summary>
|
||||
public string RequestJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
@@ -25,6 +26,7 @@ public class DailyTaskService : ApplicationService
|
||||
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
|
||||
private readonly ILogger<DailyTaskService> _logger;
|
||||
private IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetRequiredService<IDistributedLockProvider>();
|
||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||
// 任务配置
|
||||
private readonly Dictionary<int, (long RequiredTokens, long RewardTokens, string Name, string Description)>
|
||||
_taskConfigs = new()
|
||||
@@ -37,12 +39,13 @@ public class DailyTaskService : ApplicationService
|
||||
ISqlSugarRepository<DailyTaskRewardRecordAggregateRoot> dailyTaskRepository,
|
||||
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
|
||||
ILogger<DailyTaskService> logger)
|
||||
ILogger<DailyTaskService> logger, ISqlSugarRepository<AiModelEntity> aiModelRepository)
|
||||
{
|
||||
_dailyTaskRepository = dailyTaskRepository;
|
||||
_messageRepository = messageRepository;
|
||||
_premiumPackageRepository = premiumPackageRepository;
|
||||
_logger = logger;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -179,10 +182,16 @@ public class DailyTaskService : ApplicationService
|
||||
var tomorrow = today.AddDays(1);
|
||||
|
||||
// 查询今日所有使用尊享包模型的消息(role=system 表示消耗)
|
||||
// 先获取所有尊享模型的ModelId列表
|
||||
var premiumModelIds = await _aiModelRepository._DbQueryable
|
||||
.Where(x => x.IsPremium)
|
||||
.Select(x => x.ModelId)
|
||||
.ToListAsync();
|
||||
|
||||
var totalTokens = await _messageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.Where(x => x.Role == "system") // system角色表示实际消耗
|
||||
.Where(x => PremiumPackageConst.ModeIds.Contains(x.ModelId)) // 尊享包模型
|
||||
.Where(x => premiumModelIds.Contains(x.ModelId)) // 尊享包模型
|
||||
.Where(x => x.CreationTime >= today && x.CreationTime < tomorrow)
|
||||
.SumAsync(x => x.TokenUsage.TotalTokenCount);
|
||||
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
using Mapster;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||
using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道商管理服务实现
|
||||
/// </summary>
|
||||
[Authorize(Roles = "admin")]
|
||||
public class ChannelService : ApplicationService, IChannelService
|
||||
{
|
||||
private readonly ISqlSugarRepository<AiAppAggregateRoot, Guid> _appRepository;
|
||||
private readonly ISqlSugarRepository<AiModelEntity, Guid> _modelRepository;
|
||||
|
||||
public ChannelService(
|
||||
ISqlSugarRepository<AiAppAggregateRoot, Guid> appRepository,
|
||||
ISqlSugarRepository<AiModelEntity, Guid> modelRepository)
|
||||
{
|
||||
_appRepository = appRepository;
|
||||
_modelRepository = modelRepository;
|
||||
}
|
||||
|
||||
#region AI应用管理
|
||||
|
||||
/// <summary>
|
||||
/// 获取AI应用列表
|
||||
/// </summary>
|
||||
[HttpGet("channel/app")]
|
||||
public async Task<PagedResultDto<AiAppDto>> GetAppListAsync(AiAppGetListInput input)
|
||||
{
|
||||
RefAsync<int> total = 0;
|
||||
|
||||
var entities = await _appRepository._DbQueryable
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.SearchKey), x => x.Name.Contains(input.SearchKey))
|
||||
.OrderByDescending(x => x.OrderNum)
|
||||
.OrderByDescending(x => x.CreationTime)
|
||||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
|
||||
|
||||
var output = entities.Adapt<List<AiAppDto>>();
|
||||
return new PagedResultDto<AiAppDto>(total, output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取AI应用
|
||||
/// </summary>
|
||||
[HttpGet("channel/app/{id}")]
|
||||
public async Task<AiAppDto> GetAppByIdAsync([FromRoute]Guid id)
|
||||
{
|
||||
var entity = await _appRepository.GetByIdAsync(id);
|
||||
return entity.Adapt<AiAppDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建AI应用
|
||||
/// </summary>
|
||||
public async Task<AiAppDto> CreateAppAsync(AiAppCreateInput input)
|
||||
{
|
||||
var entity = new AiAppAggregateRoot
|
||||
{
|
||||
Name = input.Name,
|
||||
Endpoint = input.Endpoint,
|
||||
ExtraUrl = input.ExtraUrl,
|
||||
ApiKey = input.ApiKey,
|
||||
OrderNum = input.OrderNum
|
||||
};
|
||||
|
||||
await _appRepository.InsertAsync(entity);
|
||||
return entity.Adapt<AiAppDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新AI应用
|
||||
/// </summary>
|
||||
public async Task<AiAppDto> UpdateAppAsync(AiAppUpdateInput input)
|
||||
{
|
||||
var entity = await _appRepository.GetByIdAsync(input.Id);
|
||||
|
||||
entity.Name = input.Name;
|
||||
entity.Endpoint = input.Endpoint;
|
||||
entity.ExtraUrl = input.ExtraUrl;
|
||||
entity.ApiKey = input.ApiKey;
|
||||
entity.OrderNum = input.OrderNum;
|
||||
|
||||
await _appRepository.UpdateAsync(entity);
|
||||
return entity.Adapt<AiAppDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除AI应用
|
||||
/// </summary>
|
||||
[HttpDelete("channel/app/{id}")]
|
||||
public async Task DeleteAppAsync([FromRoute]Guid id)
|
||||
{
|
||||
// 检查是否有关联的模型
|
||||
var hasModels = await _modelRepository._DbQueryable
|
||||
.Where(x => x.AiAppId == id && !x.IsDeleted)
|
||||
.AnyAsync();
|
||||
|
||||
if (hasModels)
|
||||
{
|
||||
throw new Volo.Abp.UserFriendlyException("该应用下存在模型,无法删除");
|
||||
}
|
||||
|
||||
await _appRepository.DeleteAsync(id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AI模型管理
|
||||
|
||||
/// <summary>
|
||||
/// 获取AI模型列表
|
||||
/// </summary>
|
||||
[HttpGet("channel/model")]
|
||||
public async Task<PagedResultDto<AiModelDto>> GetModelListAsync(AiModelGetListInput input)
|
||||
{
|
||||
RefAsync<int> total = 0;
|
||||
|
||||
var query = _modelRepository._DbQueryable
|
||||
.Where(x => !x.IsDeleted)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.SearchKey), x =>
|
||||
x.Name.Contains(input.SearchKey) || x.ModelId.Contains(input.SearchKey))
|
||||
.WhereIF(input.AiAppId.HasValue, x => x.AiAppId == input.AiAppId.Value)
|
||||
.WhereIF(input.IsPremiumOnly == true, x => x.IsPremium);
|
||||
|
||||
var entities = await query
|
||||
.OrderBy(x => x.OrderNum)
|
||||
.OrderByDescending(x => x.Id)
|
||||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
|
||||
|
||||
var output = entities.Adapt<List<AiModelDto>>();
|
||||
return new PagedResultDto<AiModelDto>(total, output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取AI模型
|
||||
/// </summary>
|
||||
[HttpGet("channel/model/{id}")]
|
||||
public async Task<AiModelDto> GetModelByIdAsync([FromRoute]Guid id)
|
||||
{
|
||||
var entity = await _modelRepository.GetByIdAsync(id);
|
||||
return entity.Adapt<AiModelDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建AI模型
|
||||
/// </summary>
|
||||
public async Task<AiModelDto> CreateModelAsync(AiModelCreateInput input)
|
||||
{
|
||||
// 验证应用是否存在
|
||||
var appExists = await _appRepository._DbQueryable
|
||||
.Where(x => x.Id == input.AiAppId)
|
||||
.AnyAsync();
|
||||
|
||||
if (!appExists)
|
||||
{
|
||||
throw new Volo.Abp.UserFriendlyException("指定的AI应用不存在");
|
||||
}
|
||||
|
||||
var entity = new AiModelEntity
|
||||
{
|
||||
HandlerName = input.HandlerName,
|
||||
ModelId = input.ModelId,
|
||||
Name = input.Name,
|
||||
Description = input.Description,
|
||||
OrderNum = input.OrderNum,
|
||||
AiAppId = input.AiAppId,
|
||||
ExtraInfo = input.ExtraInfo,
|
||||
ModelType = input.ModelType,
|
||||
ModelApiType = input.ModelApiType,
|
||||
Multiplier = input.Multiplier,
|
||||
MultiplierShow = input.MultiplierShow,
|
||||
ProviderName = input.ProviderName,
|
||||
IconUrl = input.IconUrl,
|
||||
IsPremium = input.IsPremium,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
await _modelRepository.InsertAsync(entity);
|
||||
return entity.Adapt<AiModelDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新AI模型
|
||||
/// </summary>
|
||||
public async Task<AiModelDto> UpdateModelAsync(AiModelUpdateInput input)
|
||||
{
|
||||
var entity = await _modelRepository.GetByIdAsync(input.Id);
|
||||
|
||||
// 验证应用是否存在
|
||||
if (entity.AiAppId != input.AiAppId)
|
||||
{
|
||||
var appExists = await _appRepository._DbQueryable
|
||||
.Where(x => x.Id == input.AiAppId)
|
||||
.AnyAsync();
|
||||
|
||||
if (!appExists)
|
||||
{
|
||||
throw new Volo.Abp.UserFriendlyException("指定的AI应用不存在");
|
||||
}
|
||||
}
|
||||
|
||||
entity.HandlerName = input.HandlerName;
|
||||
entity.ModelId = input.ModelId;
|
||||
entity.Name = input.Name;
|
||||
entity.Description = input.Description;
|
||||
entity.OrderNum = input.OrderNum;
|
||||
entity.AiAppId = input.AiAppId;
|
||||
entity.ExtraInfo = input.ExtraInfo;
|
||||
entity.ModelType = input.ModelType;
|
||||
entity.ModelApiType = input.ModelApiType;
|
||||
entity.Multiplier = input.Multiplier;
|
||||
entity.MultiplierShow = input.MultiplierShow;
|
||||
entity.ProviderName = input.ProviderName;
|
||||
entity.IconUrl = input.IconUrl;
|
||||
entity.IsPremium = input.IsPremium;
|
||||
|
||||
await _modelRepository.UpdateAsync(entity);
|
||||
return entity.Adapt<AiModelDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除AI模型(软删除)
|
||||
/// </summary>
|
||||
[HttpDelete("channel/model/{id}")]
|
||||
public async Task DeleteModelAsync(Guid id)
|
||||
{
|
||||
await _modelRepository.DeleteByIdAsync(id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -40,35 +40,37 @@ namespace Yi.Framework.AiHub.Application.Services;
|
||||
public class AiChatService : ApplicationService
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||
private readonly AiBlacklistManager _aiBlacklistManager;
|
||||
private readonly ILogger<AiChatService> _logger;
|
||||
private readonly AiGateWayManager _aiGateWayManager;
|
||||
private readonly ModelManager _modelManager;
|
||||
private readonly PremiumPackageManager _premiumPackageManager;
|
||||
private readonly ChatManager _chatManager;
|
||||
private readonly TokenManager _tokenManager;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ISqlSugarRepository<AgentStoreAggregateRoot> _agentStoreRepository;
|
||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||
|
||||
public AiChatService(IHttpContextAccessor httpContextAccessor,
|
||||
AiBlacklistManager aiBlacklistManager,
|
||||
ISqlSugarRepository<AiModelEntity> aiModelRepository,
|
||||
ILogger<AiChatService> logger,
|
||||
AiGateWayManager aiGateWayManager,
|
||||
ModelManager modelManager,
|
||||
PremiumPackageManager premiumPackageManager,
|
||||
ChatManager chatManager, TokenManager tokenManager, IAccountService accountService,
|
||||
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository)
|
||||
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository, ISqlSugarRepository<AiModelEntity> aiModelRepository)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_aiBlacklistManager = aiBlacklistManager;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
_logger = logger;
|
||||
_aiGateWayManager = aiGateWayManager;
|
||||
_modelManager = modelManager;
|
||||
_premiumPackageManager = premiumPackageManager;
|
||||
_chatManager = chatManager;
|
||||
_tokenManager = tokenManager;
|
||||
_accountService = accountService;
|
||||
_agentStoreRepository = agentStoreRepository;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +88,7 @@ public class AiChatService : ApplicationService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取模型列表
|
||||
/// 获取对话模型列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<List<ModelGetListOutput>> GetModelAsync()
|
||||
@@ -98,18 +100,11 @@ public class AiChatService : ApplicationService
|
||||
.Select(x => new ModelGetListOutput
|
||||
{
|
||||
Id = x.Id,
|
||||
Category = "chat",
|
||||
ModelId = x.ModelId,
|
||||
ModelName = x.Name,
|
||||
ModelDescribe = x.Description,
|
||||
ModelPrice = 0,
|
||||
ModelType = "1",
|
||||
ModelShow = "0",
|
||||
SystemPrompt = null,
|
||||
ApiHost = null,
|
||||
ApiKey = null,
|
||||
Remark = x.Description,
|
||||
IsPremiumPackage = PremiumPackageConst.ModeIds.Contains(x.ModelId)
|
||||
IsPremiumPackage = x.IsPremium
|
||||
}).ToListAsync();
|
||||
return output;
|
||||
}
|
||||
@@ -144,19 +139,24 @@ public class AiChatService : ApplicationService
|
||||
}
|
||||
|
||||
//如果是尊享包服务,需要校验是是否尊享包足够
|
||||
if (CurrentUser.IsAuthenticated && PremiumPackageConst.ModeIds.Contains(input.Model))
|
||||
if (CurrentUser.IsAuthenticated)
|
||||
{
|
||||
// 检查尊享token包用量
|
||||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(CurrentUser.GetId());
|
||||
if (availableTokens <= 0)
|
||||
var isPremium = await _modelManager.IsPremiumModelAsync(input.Model);
|
||||
|
||||
if (isPremium)
|
||||
{
|
||||
throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包");
|
||||
// 检查尊享token包用量
|
||||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(CurrentUser.GetId());
|
||||
if (availableTokens <= 0)
|
||||
{
|
||||
throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//ai网关代理httpcontext
|
||||
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
||||
CurrentUser.Id, sessionId, null, cancellationToken);
|
||||
CurrentUser.Id, sessionId, null, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -192,7 +192,7 @@ public class AiChatService : ApplicationService
|
||||
|
||||
//ai网关代理httpcontext
|
||||
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
||||
CurrentUser.Id, null, null, cancellationToken);
|
||||
CurrentUser.Id, null, null, CancellationToken.None);
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ public class AiChatService : ApplicationService
|
||||
[HttpPost("ai-chat/agent/send")]
|
||||
public async Task PostAgentSendAsync([FromBody] AgentSendInput input, CancellationToken cancellationToken)
|
||||
{
|
||||
var tokenValidation = await _tokenManager.ValidateTokenAsync(input.Token, input.ModelId);
|
||||
var tokenValidation = await _tokenManager.ValidateTokenAsync(input.TokenId, input.ModelId);
|
||||
|
||||
await _aiBlacklistManager.VerifiyAiBlacklist(tokenValidation.UserId);
|
||||
// 验证用户是否为VIP
|
||||
@@ -219,7 +219,9 @@ public class AiChatService : ApplicationService
|
||||
}
|
||||
|
||||
//如果是尊享包服务,需要校验是是否尊享包足够
|
||||
if (PremiumPackageConst.ModeIds.Contains(input.ModelId))
|
||||
var isPremium = await _modelManager.IsPremiumModelAsync(input.ModelId);
|
||||
|
||||
if (isPremium)
|
||||
{
|
||||
// 检查尊享token包用量
|
||||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(tokenValidation.UserId);
|
||||
@@ -232,12 +234,12 @@ public class AiChatService : ApplicationService
|
||||
await _chatManager.AgentCompleteChatStreamAsync(_httpContextAccessor.HttpContext,
|
||||
input.SessionId,
|
||||
input.Content,
|
||||
input.Token,
|
||||
tokenValidation.Token,
|
||||
tokenValidation.TokenId,
|
||||
input.ModelId,
|
||||
tokenValidation.UserId,
|
||||
input.Tools,
|
||||
cancellationToken);
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,14 +3,17 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SqlSugar;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.BackgroundJobs;
|
||||
using Volo.Abp.Guids;
|
||||
using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||
using Yi.Framework.AiHub.Application.Jobs;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
@@ -29,23 +32,31 @@ public class AiImageService : ApplicationService
|
||||
private readonly IBackgroundJobManager _backgroundJobManager;
|
||||
private readonly AiBlacklistManager _aiBlacklistManager;
|
||||
private readonly PremiumPackageManager _premiumPackageManager;
|
||||
private readonly ModelManager _modelManager;
|
||||
private readonly IGuidGenerator _guidGenerator;
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
private readonly TokenManager _tokenManager;
|
||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||
|
||||
public AiImageService(
|
||||
ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageTaskRepository,
|
||||
IBackgroundJobManager backgroundJobManager,
|
||||
AiBlacklistManager aiBlacklistManager,
|
||||
PremiumPackageManager premiumPackageManager,
|
||||
ModelManager modelManager,
|
||||
IGuidGenerator guidGenerator,
|
||||
IWebHostEnvironment webHostEnvironment)
|
||||
IWebHostEnvironment webHostEnvironment, TokenManager tokenManager,
|
||||
ISqlSugarRepository<AiModelEntity> aiModelRepository)
|
||||
{
|
||||
_imageTaskRepository = imageTaskRepository;
|
||||
_backgroundJobManager = backgroundJobManager;
|
||||
_aiBlacklistManager = aiBlacklistManager;
|
||||
_premiumPackageManager = premiumPackageManager;
|
||||
_modelManager = modelManager;
|
||||
_guidGenerator = guidGenerator;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
_tokenManager = tokenManager;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,14 +73,22 @@ public class AiImageService : ApplicationService
|
||||
// 黑名单校验
|
||||
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||
|
||||
//校验token
|
||||
if (input.TokenId is not null)
|
||||
{
|
||||
await _tokenManager.ValidateTokenAsync(input.TokenId, input.ModelId);
|
||||
}
|
||||
|
||||
|
||||
// VIP校验
|
||||
if (!CurrentUser.IsAiVip())
|
||||
{
|
||||
throw new UserFriendlyException("图片生成功能需要VIP用户才能使用,请购买VIP后重新登录重试");
|
||||
}
|
||||
|
||||
// 尊享包校验
|
||||
if (PremiumPackageConst.ModeIds.Contains(input.ModelId))
|
||||
// 尊享包校验 - 使用ModelManager统一判断
|
||||
var isPremium = await _modelManager.IsPremiumModelAsync(input.ModelId);
|
||||
if (isPremium)
|
||||
{
|
||||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
|
||||
if (availableTokens <= 0)
|
||||
@@ -82,32 +101,23 @@ public class AiImageService : ApplicationService
|
||||
var task = new ImageStoreTaskAggregateRoot
|
||||
{
|
||||
Prompt = input.Prompt,
|
||||
ReferenceImagesBase64 = input.ReferenceImagesBase64 ?? new List<string>(),
|
||||
ReferenceImagesPrefixBase64 = input.ReferenceImagesPrefixBase64 ?? new List<string>(),
|
||||
ReferenceImagesUrl = new List<string>(),
|
||||
TaskStatus = TaskStatusEnum.Processing,
|
||||
UserId = userId
|
||||
UserId = userId,
|
||||
UserName = CurrentUser.UserName,
|
||||
TokenId = input.TokenId,
|
||||
ModelId = input.ModelId
|
||||
};
|
||||
|
||||
await _imageTaskRepository.InsertAsync(task);
|
||||
var taskId = task.Id;
|
||||
|
||||
// 构建请求JSON
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
prompt = input.Prompt,
|
||||
referenceImages = input.ReferenceImagesBase64
|
||||
});
|
||||
|
||||
// 入队后台任务
|
||||
await _backgroundJobManager.EnqueueAsync(new ImageGenerationJobArgs
|
||||
{
|
||||
TaskId = taskId,
|
||||
ModelId = input.ModelId,
|
||||
RequestJson = requestJson,
|
||||
UserId = userId
|
||||
TaskId = task.Id,
|
||||
});
|
||||
|
||||
return taskId;
|
||||
return task.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -130,12 +140,15 @@ public class AiImageService : ApplicationService
|
||||
{
|
||||
Id = task.Id,
|
||||
Prompt = task.Prompt,
|
||||
ReferenceImagesBase64 = task.ReferenceImagesBase64,
|
||||
ReferenceImagesUrl = task.ReferenceImagesUrl,
|
||||
StoreBase64 = task.StoreBase64,
|
||||
// ReferenceImagesBase64 = task.ReferenceImagesBase64,
|
||||
// ReferenceImagesUrl = task.ReferenceImagesUrl,
|
||||
// StoreBase64 = task.StoreBase64,
|
||||
StoreUrl = task.StoreUrl,
|
||||
TaskStatus = task.TaskStatus,
|
||||
CreationTime = task.CreationTime
|
||||
PublishStatus = task.PublishStatus,
|
||||
Categories = task.Categories,
|
||||
CreationTime = task.CreationTime,
|
||||
ErrorInfo = task.ErrorInfo,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,6 +158,7 @@ public class AiImageService : ApplicationService
|
||||
/// <param name="base64Data">Base64图片数据(包含前缀如 data:image/png;base64,)</param>
|
||||
/// <returns>图片访问URL</returns>
|
||||
[HttpPost("ai-image/upload-base64")]
|
||||
[AllowAnonymous]
|
||||
public async Task<string> UploadBase64ToUrlAsync([FromBody] string base64Data)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(base64Data))
|
||||
@@ -167,6 +181,7 @@ public class AiImageService : ApplicationService
|
||||
{
|
||||
mimeType = header.Split(':')[1].Split(';')[0];
|
||||
}
|
||||
|
||||
base64Content = parts[1];
|
||||
}
|
||||
}
|
||||
@@ -193,57 +208,166 @@ public class AiImageService : ApplicationService
|
||||
throw new UserFriendlyException("Base64格式无效");
|
||||
}
|
||||
|
||||
// 创建存储目录
|
||||
var uploadPath = Path.Combine(_webHostEnvironment.ContentRootPath, "wwwroot", "ai-images");
|
||||
// ==============================
|
||||
// ✅ 按日期创建目录(yyyyMMdd)
|
||||
// ==============================
|
||||
var dateFolder = DateTime.Now.ToString("yyyyMMdd");
|
||||
var uploadPath = Path.Combine(
|
||||
_webHostEnvironment.ContentRootPath,
|
||||
"wwwroot",
|
||||
"ai-images",
|
||||
dateFolder
|
||||
);
|
||||
|
||||
if (!Directory.Exists(uploadPath))
|
||||
{
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
}
|
||||
|
||||
// 生成文件名并保存
|
||||
// 保存文件
|
||||
var fileId = _guidGenerator.Create();
|
||||
var fileName = $"{fileId}{extension}";
|
||||
var filePath = Path.Combine(uploadPath, fileName);
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, imageBytes);
|
||||
|
||||
// 返回访问URL
|
||||
return $"/ai-images/{fileName}";
|
||||
// 返回包含日期目录的访问URL
|
||||
return $"/wwwroot/ai-images/{dateFolder}/{fileName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询任务列表
|
||||
/// 分页查询我的任务列表
|
||||
/// </summary>
|
||||
/// <param name="input">分页查询参数</param>
|
||||
/// <returns>任务列表</returns>
|
||||
[HttpGet("ai-image/tasks")]
|
||||
public async Task<PagedResult<ImageTaskOutput>> GetTaskPageAsync([FromQuery] ImageTaskPageInput input)
|
||||
[HttpGet("ai-image/my-tasks")]
|
||||
public async Task<PagedResult<ImageTaskOutput>> GetMyTaskPageAsync([FromQuery] ImageMyTaskPageInput input)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
|
||||
var query = _imageTaskRepository._DbQueryable
|
||||
RefAsync<int> total = 0;
|
||||
var output = await _imageTaskRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.WhereIF(input.TaskStatus.HasValue, x => x.TaskStatus == input.TaskStatus!.Value)
|
||||
.OrderByDescending(x => x.CreationTime);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query
|
||||
.Skip((input.PageIndex - 1) * input.PageSize)
|
||||
.Take(input.PageSize)
|
||||
.WhereIF(input.TaskStatus is not null, x => x.TaskStatus == input.TaskStatus)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Prompt), x => x.Prompt.Contains(input.Prompt))
|
||||
.WhereIF(input.PublishStatus is not null, x => x.PublishStatus == input.PublishStatus)
|
||||
.WhereIF(input.StartTime is not null && input.EndTime is not null,
|
||||
x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime)
|
||||
.OrderByDescending(x => x.CreationTime)
|
||||
.Select(x => new ImageTaskOutput
|
||||
{
|
||||
Id = x.Id,
|
||||
Prompt = x.Prompt,
|
||||
ReferenceImagesBase64 = x.ReferenceImagesBase64,
|
||||
ReferenceImagesUrl = x.ReferenceImagesUrl,
|
||||
StoreBase64 = x.StoreBase64,
|
||||
StoreUrl = x.StoreUrl,
|
||||
TaskStatus = x.TaskStatus,
|
||||
CreationTime = x.CreationTime
|
||||
PublishStatus = x.PublishStatus,
|
||||
Categories = x.Categories,
|
||||
CreationTime = x.CreationTime,
|
||||
ErrorInfo = x.ErrorInfo
|
||||
})
|
||||
.ToListAsync();
|
||||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
|
||||
|
||||
return new PagedResult<ImageTaskOutput>(total, items);
|
||||
|
||||
return new PagedResult<ImageTaskOutput>(total, output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询图片广场(已发布的图片)
|
||||
/// </summary>
|
||||
[HttpGet("ai-image/plaza")]
|
||||
[AllowAnonymous]
|
||||
public async Task<PagedResult<ImageTaskOutput>> GetPlazaPageAsync([FromQuery] ImagePlazaPageInput input)
|
||||
{
|
||||
RefAsync<int> total = 0;
|
||||
var output = await _imageTaskRepository._DbQueryable
|
||||
.Where(x => x.PublishStatus == PublishStatusEnum.Published)
|
||||
.Where(x => x.TaskStatus == TaskStatusEnum.Success)
|
||||
.WhereIF(input.TaskStatus is not null, x => x.TaskStatus == input.TaskStatus)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Prompt), x => x.Prompt.Contains(input.Prompt))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Categories), x => SqlFunc.JsonLike(x.Categories, input.Categories))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.UserName),x=>x.UserName.Contains(input.UserName) )
|
||||
.WhereIF(input.StartTime is not null && input.EndTime is not null,
|
||||
x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime)
|
||||
.OrderByDescending(x => x.CreationTime)
|
||||
.Select(x => new ImageTaskOutput
|
||||
{
|
||||
Id = x.Id,
|
||||
Prompt = x.Prompt,
|
||||
IsAnonymous = x.IsAnonymous,
|
||||
StoreUrl = x.StoreUrl,
|
||||
TaskStatus = x.TaskStatus,
|
||||
PublishStatus = x.PublishStatus,
|
||||
Categories = x.Categories,
|
||||
CreationTime = x.CreationTime,
|
||||
ErrorInfo = null,
|
||||
UserName = x.UserName,
|
||||
UserId = x.UserId,
|
||||
|
||||
})
|
||||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); ;
|
||||
|
||||
|
||||
output.ForEach(x =>
|
||||
{
|
||||
if (x.IsAnonymous)
|
||||
{
|
||||
x.UserName = null;
|
||||
x.UserId = null;
|
||||
}
|
||||
});
|
||||
|
||||
return new PagedResult<ImageTaskOutput>(total, output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布图片到广场
|
||||
/// </summary>
|
||||
[HttpPost("ai-image/publish")]
|
||||
public async Task PublishAsync([FromBody] PublishImageInput input)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
|
||||
var task = await _imageTaskRepository.GetFirstAsync(x => x.Id == input.TaskId && x.UserId == userId);
|
||||
if (task == null)
|
||||
{
|
||||
throw new UserFriendlyException("任务不存在或无权访问");
|
||||
}
|
||||
|
||||
if (task.TaskStatus != TaskStatusEnum.Success)
|
||||
{
|
||||
throw new UserFriendlyException("只有已完成的任务才能发布");
|
||||
}
|
||||
|
||||
if (task.PublishStatus == PublishStatusEnum.Published)
|
||||
{
|
||||
throw new UserFriendlyException("该任务已发布");
|
||||
}
|
||||
|
||||
//设置发布
|
||||
task.SetPublish(input.IsAnonymous,input.Categories);
|
||||
await _imageTaskRepository.UpdateAsync(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取图片模型列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("ai-image/model")]
|
||||
[AllowAnonymous]
|
||||
public async Task<List<ModelGetListOutput>> GetModelAsync()
|
||||
{
|
||||
var output = await _aiModelRepository._DbQueryable
|
||||
.Where(x => x.ModelType == ModelTypeEnum.Image)
|
||||
.Where(x => x.ModelApiType == ModelApiTypeEnum.GenerateContent)
|
||||
.OrderByDescending(x => x.OrderNum)
|
||||
.Select(x => new ModelGetListOutput
|
||||
{
|
||||
Id = x.Id,
|
||||
ModelId = x.ModelId,
|
||||
ModelName = x.Name,
|
||||
ModelDescribe = x.Description,
|
||||
Remark = x.Description,
|
||||
IsPremiumPackage = x.IsPremium
|
||||
}).ToListAsync();
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,4 +392,4 @@ public class PagedResult<T>
|
||||
Total = total;
|
||||
Items = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using Mapster;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Model;
|
||||
using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Extensions;
|
||||
@@ -18,10 +20,12 @@ namespace Yi.Framework.AiHub.Application.Services.Chat;
|
||||
public class ModelService : ApplicationService, IModelService
|
||||
{
|
||||
private readonly ISqlSugarRepository<AiModelEntity, Guid> _modelRepository;
|
||||
private readonly ModelManager _modelManager;
|
||||
|
||||
public ModelService(ISqlSugarRepository<AiModelEntity, Guid> modelRepository)
|
||||
public ModelService(ISqlSugarRepository<AiModelEntity, Guid> modelRepository, ModelManager modelManager)
|
||||
{
|
||||
_modelRepository = modelRepository;
|
||||
_modelManager = modelManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,8 +45,7 @@ public class ModelService : ApplicationService, IModelService
|
||||
input.ModelTypes.Contains(x.ModelType))
|
||||
.WhereIF(input.ModelApiTypes is not null, x =>
|
||||
input.ModelApiTypes.Contains(x.ModelApiType))
|
||||
.WhereIF(input.IsPremiumOnly == true, x =>
|
||||
PremiumPackageConst.ModeIds.Contains(x.ModelId))
|
||||
.WhereIF(input.IsPremiumOnly == true, x => x.IsPremium)
|
||||
.GroupBy(x => x.ModelId)
|
||||
.Select(x => x.ModelId)
|
||||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total));
|
||||
@@ -61,7 +64,7 @@ public class ModelService : ApplicationService, IModelService
|
||||
MultiplierShow = x.First().MultiplierShow,
|
||||
ProviderName = x.First().ProviderName,
|
||||
IconUrl = x.First().IconUrl,
|
||||
IsPremium = PremiumPackageConst.ModeIds.Contains(x.First().ModelId),
|
||||
IsPremium = x.First().IsPremium,
|
||||
OrderNum = x.First().OrderNum
|
||||
}).ToList();
|
||||
|
||||
@@ -77,11 +80,11 @@ public class ModelService : ApplicationService, IModelService
|
||||
.Where(x => !x.IsDeleted)
|
||||
.Where(x => !string.IsNullOrEmpty(x.ProviderName))
|
||||
.GroupBy(x => x.ProviderName)
|
||||
.OrderBy(x => x.ProviderName)
|
||||
.OrderBy(x => x.OrderNum)
|
||||
.Select(x => x.ProviderName)
|
||||
.ToListAsync();
|
||||
|
||||
return providers;
|
||||
return providers!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,4 +118,13 @@ public class ModelService : ApplicationService, IModelService
|
||||
|
||||
return Task.FromResult(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除尊享模型ID缓存
|
||||
/// </summary>
|
||||
[HttpPost("model/clear-premium-cache")]
|
||||
public async Task ClearPremiumModelCacheAsync()
|
||||
{
|
||||
await _modelManager.ClearPremiumModelIdsCacheAsync();
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
@@ -23,13 +25,16 @@ public class TokenService : ApplicationService
|
||||
{
|
||||
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
||||
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
||||
private readonly ModelManager _modelManager;
|
||||
|
||||
public TokenService(
|
||||
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
|
||||
ModelManager modelManager)
|
||||
{
|
||||
_tokenRepository = tokenRepository;
|
||||
_usageStatisticsRepository = usageStatisticsRepository;
|
||||
_modelManager = modelManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,8 +56,8 @@ public class TokenService : ApplicationService
|
||||
return new PagedResultDto<TokenGetListOutputDto>();
|
||||
}
|
||||
|
||||
// 获取尊享包模型ID列表
|
||||
var premiumModelIds = PremiumPackageConst.ModeIds;
|
||||
// 通过ModelManager获取尊享包模型ID列表
|
||||
var premiumModelIds = await _modelManager.GetPremiumModelIdsAsync();
|
||||
|
||||
// 批量查询所有Token的尊享包已使用额度
|
||||
var tokenIds = tokens.Select(t => t.Id).ToList();
|
||||
@@ -86,7 +91,7 @@ public class TokenService : ApplicationService
|
||||
}
|
||||
|
||||
[HttpGet("token/select-list")]
|
||||
public async Task<List<TokenSelectListOutputDto>> GetSelectListAsync()
|
||||
public async Task<List<TokenSelectListOutputDto>> GetSelectListAsync([FromQuery] bool? includeDefault = true)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
var tokens = await _tokenRepository._DbQueryable
|
||||
@@ -99,13 +104,17 @@ public class TokenService : ApplicationService
|
||||
Name = x.Name,
|
||||
IsDisabled = x.IsDisabled
|
||||
}).ToListAsync();
|
||||
|
||||
tokens.Insert(0,new TokenSelectListOutputDto
|
||||
|
||||
if (includeDefault == true)
|
||||
{
|
||||
TokenId = Guid.Empty,
|
||||
Name = "默认",
|
||||
IsDisabled = false
|
||||
});
|
||||
tokens.Insert(0, new TokenSelectListOutputDto
|
||||
{
|
||||
TokenId = Guid.Empty,
|
||||
Name = "默认",
|
||||
IsDisabled = false
|
||||
});
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,25 +27,27 @@ public class OpenApiService : ApplicationService
|
||||
private readonly ILogger<OpenApiService> _logger;
|
||||
private readonly TokenManager _tokenManager;
|
||||
private readonly AiGateWayManager _aiGateWayManager;
|
||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||
private readonly ModelManager _modelManager;
|
||||
private readonly AiBlacklistManager _aiBlacklistManager;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly PremiumPackageManager _premiumPackageManager;
|
||||
private readonly ISqlSugarRepository<ImageStoreTaskAggregateRoot> _imageStoreRepository;
|
||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
|
||||
TokenManager tokenManager, AiGateWayManager aiGateWayManager,
|
||||
ISqlSugarRepository<AiModelEntity> aiModelRepository, AiBlacklistManager aiBlacklistManager,
|
||||
IAccountService accountService, PremiumPackageManager premiumPackageManager, ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageStoreRepository)
|
||||
ModelManager modelManager, AiBlacklistManager aiBlacklistManager,
|
||||
IAccountService accountService, PremiumPackageManager premiumPackageManager, ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageStoreRepository, ISqlSugarRepository<AiModelEntity> aiModelRepository)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = logger;
|
||||
_tokenManager = tokenManager;
|
||||
_aiGateWayManager = aiGateWayManager;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
_modelManager = modelManager;
|
||||
_aiBlacklistManager = aiBlacklistManager;
|
||||
_accountService = accountService;
|
||||
_premiumPackageManager = premiumPackageManager;
|
||||
_imageStoreRepository = imageStoreRepository;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,7 +67,9 @@ public class OpenApiService : ApplicationService
|
||||
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||
|
||||
//如果是尊享包服务,需要校验是是否尊享包足够
|
||||
if (PremiumPackageConst.ModeIds.Contains(input.Model))
|
||||
var isPremium = await _modelManager.IsPremiumModelAsync(input.Model);
|
||||
|
||||
if (isPremium)
|
||||
{
|
||||
// 检查尊享token包用量
|
||||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
|
||||
@@ -79,13 +83,13 @@ public class OpenApiService : ApplicationService
|
||||
if (input.Stream == true)
|
||||
{
|
||||
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
||||
userId, null, tokenId, cancellationToken);
|
||||
userId, null, tokenId,CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId,
|
||||
null, tokenId,
|
||||
cancellationToken);
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,14 +197,14 @@ public class OpenApiService : ApplicationService
|
||||
{
|
||||
await _aiGateWayManager.AnthropicCompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
|
||||
input,
|
||||
userId, null, tokenId, cancellationToken);
|
||||
userId, null, tokenId, CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _aiGateWayManager.AnthropicCompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
||||
userId,
|
||||
null, tokenId,
|
||||
cancellationToken);
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,14 +249,14 @@ public class OpenApiService : ApplicationService
|
||||
{
|
||||
await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
|
||||
input,
|
||||
userId, null, tokenId, cancellationToken);
|
||||
userId, null, tokenId, CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _aiGateWayManager.OpenAiResponsesAsyncForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
||||
userId,
|
||||
null, tokenId,
|
||||
cancellationToken);
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +308,7 @@ public class OpenApiService : ApplicationService
|
||||
modelId, input,
|
||||
userId,
|
||||
null, tokenId,
|
||||
cancellationToken);
|
||||
CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -312,7 +316,7 @@ public class OpenApiService : ApplicationService
|
||||
modelId, input,
|
||||
userId,
|
||||
null, tokenId,
|
||||
cancellationToken);
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
|
||||
using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
@@ -27,17 +29,19 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
|
||||
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
||||
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
|
||||
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
||||
|
||||
private readonly ModelManager _modelManager;
|
||||
public UsageStatisticsService(
|
||||
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
|
||||
ISqlSugarRepository<TokenAggregateRoot> tokenRepository)
|
||||
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
|
||||
ModelManager modelManager)
|
||||
{
|
||||
_messageRepository = messageRepository;
|
||||
_usageStatisticsRepository = usageStatisticsRepository;
|
||||
_premiumPackageRepository = premiumPackageRepository;
|
||||
_tokenRepository = tokenRepository;
|
||||
_modelManager = modelManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -181,7 +185,9 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
|
||||
public async Task<List<TokenPremiumUsageDto>> GetPremiumTokenUsageByTokenAsync()
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
var premiumModelIds = PremiumPackageConst.ModeIds;
|
||||
|
||||
// 通过ModelManager获取所有尊享模型的ModelId列表
|
||||
var premiumModelIds = await _modelManager.GetPremiumModelIdsAsync();
|
||||
|
||||
// 从UsageStatistics表获取尊享模型的token消耗统计(按TokenId聚合)
|
||||
var tokenUsages = await _usageStatisticsRepository._DbQueryable
|
||||
|
||||
@@ -20,7 +20,6 @@ public static class GeminiGenerateContentAcquirer
|
||||
+ usage.Value.GetPath("thoughtsTokenCount").GetInt()
|
||||
+ usage.Value.GetPath("toolUsePromptTokenCount").GetInt();
|
||||
|
||||
|
||||
return new ThorUsageResponse
|
||||
{
|
||||
PromptTokens = inputTokens,
|
||||
@@ -32,14 +31,47 @@ public static class GeminiGenerateContentAcquirer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取图片url,包含前缀
|
||||
/// 获取图片 base64(包含 data:image 前缀)
|
||||
/// 优先从 inlineData.data 中获取,其次从 markdown text 中解析
|
||||
/// </summary>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetImageBase64(JsonElement response)
|
||||
public static string GetImagePrefixBase64(JsonElement response)
|
||||
{
|
||||
//todo
|
||||
//获取他的base64字符串
|
||||
return string.Empty;
|
||||
// Step 1: 优先尝试从 candidates[0].content.parts[0].inlineData.data 获取
|
||||
var inlineBase64 = response
|
||||
.GetPath("candidates", 0, "content", "parts", 0, "inlineData", "data")
|
||||
.GetString();
|
||||
|
||||
if (!string.IsNullOrEmpty(inlineBase64))
|
||||
{
|
||||
// 默认按 png 格式拼接前缀
|
||||
return $"data:image/png;base64,{inlineBase64}";
|
||||
}
|
||||
|
||||
// Step 2: fallback,从 candidates[0].content.parts[0].text 中解析 markdown 图片
|
||||
var text = response
|
||||
.GetPath("candidates", 0, "content", "parts", 0, "text")
|
||||
.GetString();
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// markdown 图片格式: 
|
||||
var startMarker = "(data:image/";
|
||||
var startIndex = text.IndexOf(startMarker, StringComparison.Ordinal);
|
||||
if (startIndex < 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
startIndex += 1; // 跳过 "("
|
||||
var endIndex = text.IndexOf(')', startIndex);
|
||||
if (endIndex <= startIndex)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return text.Substring(startIndex, endIndex - startIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 发布状态枚举
|
||||
/// </summary>
|
||||
public enum PublishStatusEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 未发布
|
||||
/// </summary>
|
||||
Unpublished = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 已发布
|
||||
/// </summary>
|
||||
Published = 1
|
||||
}
|
||||
@@ -107,35 +107,6 @@ public class AzureDatabricksChatCompletionsService(ILogger<AzureDatabricksChatCo
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// var content = result?.Choices?.FirstOrDefault()?.Delta;
|
||||
//
|
||||
// if (first && content?.Content == OpenAIConstant.ThinkStart)
|
||||
// {
|
||||
// isThink = true;
|
||||
// continue;
|
||||
// // 需要将content的内容转换到其他字段
|
||||
// }
|
||||
//
|
||||
// if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true)
|
||||
// {
|
||||
// isThink = false;
|
||||
// // 需要将content的内容转换到其他字段
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (isThink && result?.Choices != null)
|
||||
// {
|
||||
// // 需要将content的内容转换到其他字段
|
||||
// foreach (var choice in result.Choices)
|
||||
// {
|
||||
// choice.Delta.ReasoningContent = choice.Delta.Content;
|
||||
// choice.Delta.Content = string.Empty;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// first = false;
|
||||
|
||||
yield return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats;
|
||||
|
||||
public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsService> logger,IHttpClientFactory httpClientFactory)
|
||||
public sealed class OpenAiChatCompletionsService(
|
||||
ILogger<OpenAiChatCompletionsService> logger,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
: IChatCompletionService
|
||||
{
|
||||
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
|
||||
@@ -19,8 +21,18 @@ public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsSe
|
||||
using var openai =
|
||||
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
|
||||
|
||||
var endpoint = options?.Endpoint.TrimEnd('/');
|
||||
|
||||
//兼容 v1结尾
|
||||
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
|
||||
}
|
||||
|
||||
var requestUri = endpoint + "/v1/chat/completions";
|
||||
|
||||
var response = await httpClientFactory.CreateClient().HttpRequestRaw(
|
||||
options?.Endpoint.TrimEnd('/') + "/chat/completions",
|
||||
requestUri,
|
||||
chatCompletionCreate, options.ApiKey);
|
||||
|
||||
openai?.SetTag("Model", chatCompletionCreate.Model);
|
||||
@@ -130,8 +142,16 @@ public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsSe
|
||||
using var openai =
|
||||
Activity.Current?.Source.StartActivity("OpenAI 对话补全");
|
||||
|
||||
var endpoint = options?.Endpoint.TrimEnd('/');
|
||||
|
||||
//兼容 v1结尾
|
||||
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
|
||||
}
|
||||
var requestUri = endpoint + "/v1/chat/completions";
|
||||
var response = await httpClientFactory.CreateClient().PostJsonAsync(
|
||||
options?.Endpoint.TrimEnd('/') + "/chat/completions",
|
||||
requestUri,
|
||||
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
|
||||
|
||||
openai?.SetTag("Model", chatCompletionCreate.Model);
|
||||
@@ -152,7 +172,8 @@ public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsSe
|
||||
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint,
|
||||
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
|
||||
options.Endpoint,
|
||||
response.StatusCode, error);
|
||||
|
||||
throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString());
|
||||
|
||||
@@ -22,7 +22,16 @@ public class OpenAiResponseService(ILogger<OpenAiResponseService> logger,IHttpCl
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
|
||||
var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/responses", input, options.ApiKey);
|
||||
var endpoint = options?.Endpoint.TrimEnd('/');
|
||||
|
||||
//兼容 v1结尾
|
||||
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
|
||||
}
|
||||
var requestUri = endpoint + "/v1/responses";
|
||||
|
||||
var response = await client.HttpRequestRaw(requestUri, input, options.ApiKey);
|
||||
|
||||
openai?.SetTag("Model", input.Model);
|
||||
openai?.SetTag("Response", response.StatusCode.ToString());
|
||||
@@ -86,8 +95,17 @@ public class OpenAiResponseService(ILogger<OpenAiResponseService> logger,IHttpCl
|
||||
using var openai =
|
||||
Activity.Current?.Source.StartActivity("OpenAI 响应");
|
||||
|
||||
var endpoint = options?.Endpoint.TrimEnd('/');
|
||||
|
||||
//兼容 v1结尾
|
||||
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
|
||||
}
|
||||
var requestUri = endpoint + "/v1/responses";
|
||||
|
||||
var response = await httpClientFactory.CreateClient().PostJsonAsync(
|
||||
options?.Endpoint.TrimEnd('/') + "/responses",
|
||||
requestUri,
|
||||
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
|
||||
|
||||
openai?.SetTag("Model", chatCompletionCreate.Model);
|
||||
|
||||
@@ -23,9 +23,17 @@ public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletio
|
||||
|
||||
using var openai =
|
||||
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
|
||||
|
||||
|
||||
var endpoint = options?.Endpoint.TrimEnd('/');
|
||||
//兼容 v1结尾
|
||||
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
|
||||
}
|
||||
var requestUri = endpoint + "/v1/chat/completions";
|
||||
|
||||
var response = await httpClientFactory.CreateClient().HttpRequestRaw(
|
||||
options?.Endpoint.TrimEnd('/') + "/chat/completions",
|
||||
requestUri,
|
||||
chatCompletionCreate, options.ApiKey);
|
||||
|
||||
openai?.SetTag("Model", chatCompletionCreate.Model);
|
||||
@@ -92,40 +100,6 @@ public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletio
|
||||
|
||||
var result = JsonSerializer.Deserialize<ThorChatCompletionsResponse>(line,
|
||||
ThorJsonSerializer.DefaultOptions);
|
||||
|
||||
// var content = result?.Choices?.FirstOrDefault()?.Delta;
|
||||
//
|
||||
// // if (first && string.IsNullOrWhiteSpace(content?.Content) && string.IsNullOrEmpty(content?.ReasoningContent))
|
||||
// // {
|
||||
// // continue;
|
||||
// // }
|
||||
//
|
||||
// if (first && content.Content == OpenAIConstant.ThinkStart)
|
||||
// {
|
||||
// isThink = true;
|
||||
// //continue;
|
||||
// // 需要将content的内容转换到其他字段
|
||||
// }
|
||||
//
|
||||
// if (isThink && content.Content.Contains(OpenAIConstant.ThinkEnd))
|
||||
// {
|
||||
// isThink = false;
|
||||
// // 需要将content的内容转换到其他字段
|
||||
// //continue;
|
||||
// }
|
||||
//
|
||||
// if (isThink)
|
||||
// {
|
||||
// // 需要将content的内容转换到其他字段
|
||||
// foreach (var choice in result.Choices)
|
||||
// {
|
||||
// //choice.Delta.ReasoningContent = choice.Delta.Content;
|
||||
// //choice.Delta.Content = string.Empty;
|
||||
// }
|
||||
// }
|
||||
|
||||
// first = false;
|
||||
|
||||
yield return result;
|
||||
}
|
||||
}
|
||||
@@ -142,8 +116,16 @@ public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletio
|
||||
options.Endpoint = "https://api.deepseek.com/v1";
|
||||
}
|
||||
|
||||
var endpoint = options?.Endpoint.TrimEnd('/');
|
||||
//兼容 v1结尾
|
||||
if (endpoint != null && endpoint.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
endpoint = endpoint.Substring(0, endpoint.Length - "/v1".Length);
|
||||
}
|
||||
var requestUri = endpoint + "/v1/chat/completions";
|
||||
|
||||
var response = await httpClientFactory.CreateClient().PostJsonAsync(
|
||||
options?.Endpoint.TrimEnd('/') + "/chat/completions",
|
||||
requestUri,
|
||||
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
|
||||
|
||||
openai?.SetTag("Model", chatCompletionCreate.Model);
|
||||
|
||||
@@ -14,23 +14,18 @@ public class ImageStoreTaskAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
public string Prompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 参考图Base64
|
||||
/// 参考图PrefixBase64(带前缀,如 data:image/png;base64,xxx)
|
||||
/// </summary>
|
||||
[SugarColumn(IsJson = true)]
|
||||
public List<string> ReferenceImagesBase64 { get; set; }
|
||||
[SugarColumn(IsJson = true, ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||
public List<string> ReferenceImagesPrefixBase64 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 参考图url
|
||||
/// </summary>
|
||||
[SugarColumn(IsJson = true)]
|
||||
public List<string> ReferenceImagesUrl { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 图片base64
|
||||
/// </summary>
|
||||
public string? StoreBase64 { get; set; }
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 图片绝对路径
|
||||
/// </summary>
|
||||
@@ -46,6 +41,43 @@ public class ImageStoreTaskAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户名称
|
||||
/// </summary>
|
||||
public string? UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型id
|
||||
/// </summary>
|
||||
public string ModelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||
public string? ErrorInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发布状态
|
||||
/// </summary>
|
||||
public PublishStatusEnum PublishStatus { get; set; } = PublishStatusEnum.Unpublished;
|
||||
|
||||
/// <summary>
|
||||
/// 分类标签
|
||||
/// </summary>
|
||||
[SugarColumn(IsJson = true)]
|
||||
public List<string> Categories { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 是否匿名
|
||||
/// </summary>
|
||||
public bool IsAnonymous { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 密钥id
|
||||
/// </summary>
|
||||
public Guid? TokenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置成功
|
||||
/// </summary>
|
||||
@@ -55,4 +87,18 @@ public class ImageStoreTaskAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
TaskStatus = TaskStatusEnum.Success;
|
||||
StoreUrl = storeUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置发布
|
||||
/// </summary>
|
||||
/// <param name="isAnonymous"></param>
|
||||
/// <param name="categories"></param>
|
||||
public void SetPublish(bool isAnonymous,List<string> categories)
|
||||
{
|
||||
this.PublishStatus = PublishStatusEnum.Published;
|
||||
this.IsAnonymous = isAnonymous;
|
||||
this.Categories = categories;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -80,4 +80,9 @@ public class AiModelEntity : Entity<Guid>, IOrderNum, ISoftDelete
|
||||
/// 模型图标URL
|
||||
/// </summary>
|
||||
public string? IconUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为尊享模型
|
||||
/// </summary>
|
||||
public bool IsPremium { get; set; }
|
||||
}
|
||||
@@ -92,7 +92,7 @@ public class AiGateWayManager : DomainService
|
||||
{
|
||||
throw new UserFriendlyException($"【{modelId}】模型当前版本【{modelApiType}】格式不支持");
|
||||
}
|
||||
// ✅ 统一处理 -nx 后缀(网关层模型规范化)
|
||||
// ✅ 统一处理 yi- 后缀(网关层模型规范化)
|
||||
if (!string.IsNullOrEmpty(aiModelDescribe.ModelId) &&
|
||||
aiModelDescribe.ModelId.StartsWith("yi-", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -158,7 +158,12 @@ public class AiGateWayManager : DomainService
|
||||
await _usageStatisticsManager.SetUsageAsync(userId.Value, sourceModelId, data.Usage, tokenId);
|
||||
|
||||
// 扣减尊享token包用量
|
||||
if (PremiumPackageConst.ModeIds.Contains(sourceModelId))
|
||||
var isPremium = await _aiModelRepository._DbQueryable
|
||||
.Where(x => x.ModelId == request.Model)
|
||||
.Select(x => x.IsPremium)
|
||||
.FirstAsync();
|
||||
|
||||
if (isPremium)
|
||||
{
|
||||
var totalTokens = data.Usage?.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
@@ -315,12 +320,20 @@ public class AiGateWayManager : DomainService
|
||||
await _usageStatisticsManager.SetUsageAsync(userId, sourceModelId, tokenUsage, tokenId);
|
||||
|
||||
// 扣减尊享token包用量
|
||||
if (userId is not null && PremiumPackageConst.ModeIds.Contains(sourceModelId))
|
||||
if (userId is not null)
|
||||
{
|
||||
var totalTokens = tokenUsage.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
var isPremium = await _aiModelRepository._DbQueryable
|
||||
.Where(x => x.ModelId == request.Model)
|
||||
.Select(x => x.IsPremium)
|
||||
.FirstAsync();
|
||||
|
||||
if (isPremium)
|
||||
{
|
||||
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||
var totalTokens = tokenUsage.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
{
|
||||
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,12 +391,20 @@ public class AiGateWayManager : DomainService
|
||||
await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage, tokenId);
|
||||
|
||||
// 扣减尊享token包用量
|
||||
if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model))
|
||||
if (userId is not null)
|
||||
{
|
||||
var totalTokens = response.Usage.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
var isPremium = await _aiModelRepository._DbQueryable
|
||||
.Where(x => x.ModelId == request.Model)
|
||||
.Select(x => x.IsPremium)
|
||||
.FirstAsync();
|
||||
|
||||
if (isPremium)
|
||||
{
|
||||
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||
var totalTokens = response.Usage.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
{
|
||||
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -982,7 +1003,7 @@ public class AiGateWayManager : DomainService
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private const string ImageStoreHost = "http://localhost:19001/api/app";
|
||||
/// <summary>
|
||||
/// Gemini 生成(Image)-非流式-缓存处理
|
||||
/// 返回图片绝对路径
|
||||
@@ -1011,16 +1032,16 @@ public class AiGateWayManager : DomainService
|
||||
var data = await chatService.GenerateContentAsync(modelDescribe, request, cancellationToken);
|
||||
|
||||
//解析json,获取base64字符串
|
||||
var imageBase64 = GeminiGenerateContentAcquirer.GetImageBase64(data);
|
||||
var imagePrefixBase64 = GeminiGenerateContentAcquirer.GetImagePrefixBase64(data);
|
||||
|
||||
//远程调用上传接口,将base64转换为URL
|
||||
var httpClient = LazyServiceProvider.LazyGetRequiredService<IHttpClientFactory>().CreateClient();
|
||||
var uploadUrl = $"https://ccnetcore.com/prod-api/ai-hub/ai-image/upload-base64";
|
||||
var content = new StringContent(JsonSerializer.Serialize(imageBase64), Encoding.UTF8, "application/json");
|
||||
// var uploadUrl = $"https://ccnetcore.com/prod-api/ai-hub/ai-image/upload-base64";
|
||||
var uploadUrl = $"{ImageStoreHost}/ai-image/upload-base64";
|
||||
var content = new StringContent(JsonSerializer.Serialize(imagePrefixBase64), Encoding.UTF8, "application/json");
|
||||
var uploadResponse = await httpClient.PostAsync(uploadUrl, content, cancellationToken);
|
||||
uploadResponse.EnsureSuccessStatusCode();
|
||||
var storeUrl = await uploadResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||
storeUrl = storeUrl.Trim('"'); // 移除JSON字符串的引号
|
||||
|
||||
var tokenUsage = new ThorUsageResponse
|
||||
{
|
||||
@@ -1047,8 +1068,7 @@ public class AiGateWayManager : DomainService
|
||||
}
|
||||
|
||||
//设置存储base64和url
|
||||
imageStoreTask.StoreBase64 = imageBase64;
|
||||
imageStoreTask.SetSuccess(storeUrl);
|
||||
imageStoreTask.SetSuccess($"{ImageStoreHost}{storeUrl}");
|
||||
await _imageStoreTaskRepository.UpdateAsync(imageStoreTask);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using Volo.Abp.Domain.Services;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||
using Yi.Framework.AiHub.Domain.AiGateWay;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Attributes;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
@@ -34,12 +35,13 @@ public class ChatManager : DomainService
|
||||
private readonly UsageStatisticsManager _usageStatisticsManager;
|
||||
private readonly PremiumPackageManager _premiumPackageManager;
|
||||
private readonly AiGateWayManager _aiGateWayManager;
|
||||
private readonly ISqlSugarRepository<AiModelEntity, Guid> _aiModelRepository;
|
||||
|
||||
public ChatManager(ILoggerFactory loggerFactory,
|
||||
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
||||
ISqlSugarRepository<AgentStoreAggregateRoot> agentStoreRepository, AiMessageManager aiMessageManager,
|
||||
UsageStatisticsManager usageStatisticsManager, PremiumPackageManager premiumPackageManager,
|
||||
AiGateWayManager aiGateWayManager)
|
||||
AiGateWayManager aiGateWayManager, ISqlSugarRepository<AiModelEntity, Guid> aiModelRepository)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_messageRepository = messageRepository;
|
||||
@@ -48,6 +50,7 @@ public class ChatManager : DomainService
|
||||
_usageStatisticsManager = usageStatisticsManager;
|
||||
_premiumPackageManager = premiumPackageManager;
|
||||
_aiGateWayManager = aiGateWayManager;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -207,7 +210,12 @@ public class ChatManager : DomainService
|
||||
await _usageStatisticsManager.SetUsageAsync(userId, modelId, usage, tokenId);
|
||||
|
||||
//扣减尊享token包用量
|
||||
if (PremiumPackageConst.ModeIds.Contains(modelId))
|
||||
var isPremium = await _aiModelRepository._DbQueryable
|
||||
.Where(x => x.ModelId == modelId)
|
||||
.Select(x => x.IsPremium)
|
||||
.FirstAsync();
|
||||
|
||||
if (isPremium)
|
||||
{
|
||||
var totalTokens = usage?.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Volo.Abp.Caching;
|
||||
using Volo.Abp.Domain.Services;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// 模型管理器
|
||||
/// </summary>
|
||||
public class ModelManager : DomainService
|
||||
{
|
||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||
private readonly IDistributedCache<List<string>, string> _distributedCache;
|
||||
private readonly ILogger<ModelManager> _logger;
|
||||
private const string PREMIUM_MODEL_IDS_CACHE_KEY = "PremiumModelIds";
|
||||
|
||||
public ModelManager(
|
||||
ISqlSugarRepository<AiModelEntity> aiModelRepository,
|
||||
IDistributedCache<List<string>, string> distributedCache,
|
||||
ILogger<ModelManager> logger)
|
||||
{
|
||||
_aiModelRepository = aiModelRepository;
|
||||
_distributedCache = distributedCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有尊享模型ID列表(使用分布式缓存,10分钟过期)
|
||||
/// </summary>
|
||||
/// <returns>尊享模型ID列表</returns>
|
||||
public async Task<List<string>> GetPremiumModelIdsAsync()
|
||||
{
|
||||
var output = await _distributedCache.GetOrAddAsync(
|
||||
PREMIUM_MODEL_IDS_CACHE_KEY,
|
||||
async () =>
|
||||
{
|
||||
// 从数据库查询
|
||||
var premiumModelIds = await _aiModelRepository._DbQueryable
|
||||
.Where(x => x.IsPremium)
|
||||
.Select(x => x.ModelId)
|
||||
.ToListAsync();
|
||||
return premiumModelIds;
|
||||
},
|
||||
() => new Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
|
||||
}
|
||||
);
|
||||
return output ?? new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定模型是否为尊享模型
|
||||
/// </summary>
|
||||
/// <param name="modelId">模型ID</param>
|
||||
/// <returns>是否为尊享模型</returns>
|
||||
public async Task<bool> IsPremiumModelAsync(string modelId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var premiumModelIds = await GetPremiumModelIdsAsync();
|
||||
return premiumModelIds.Contains(modelId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除尊享模型ID缓存
|
||||
/// </summary>
|
||||
public async Task ClearPremiumModelIdsCacheAsync()
|
||||
{
|
||||
await _distributedCache.RemoveAsync(PREMIUM_MODEL_IDS_CACHE_KEY);
|
||||
_logger.LogInformation("已清除尊享模型ID分布式缓存");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Services;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
@@ -21,43 +22,62 @@ public class TokenValidationResult
|
||||
/// Token Id
|
||||
/// </summary>
|
||||
public Guid TokenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// token
|
||||
/// </summary>
|
||||
public string Token { get; set; }
|
||||
}
|
||||
|
||||
public class TokenManager : DomainService
|
||||
{
|
||||
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
||||
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
||||
private readonly ISqlSugarRepository<AiModelEntity, Guid> _aiModelRepository;
|
||||
|
||||
public TokenManager(
|
||||
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
|
||||
ISqlSugarRepository<AiModelEntity, Guid> aiModelRepository)
|
||||
{
|
||||
_tokenRepository = tokenRepository;
|
||||
_usageStatisticsRepository = usageStatisticsRepository;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证Token并返回用户Id和TokenId
|
||||
/// </summary>
|
||||
/// <param name="token">Token密钥</param>
|
||||
/// <param name="tokenOrId">Token密钥或者TokenId</param>
|
||||
/// <param name="modelId">模型Id(用于判断是否是尊享模型需要检查额度)</param>
|
||||
/// <returns>Token验证结果</returns>
|
||||
public async Task<TokenValidationResult> ValidateTokenAsync(string? token, string? modelId = null)
|
||||
public async Task<TokenValidationResult> ValidateTokenAsync(object tokenOrId, string? modelId = null)
|
||||
{
|
||||
if (token is null)
|
||||
|
||||
if (tokenOrId is null)
|
||||
{
|
||||
throw new UserFriendlyException("当前请求未包含token", "401");
|
||||
}
|
||||
|
||||
if (!token.StartsWith("yi-"))
|
||||
|
||||
TokenAggregateRoot entity;
|
||||
if (tokenOrId is Guid tokenId)
|
||||
{
|
||||
throw new UserFriendlyException("当前请求token非法", "401");
|
||||
entity = await _tokenRepository._DbQueryable
|
||||
.Where(x => x.Id == tokenId)
|
||||
.FirstAsync();
|
||||
}
|
||||
|
||||
var entity = await _tokenRepository._DbQueryable
|
||||
.Where(x => x.Token == token)
|
||||
.FirstAsync();
|
||||
|
||||
else
|
||||
{
|
||||
var tokenStr = tokenOrId.ToString();
|
||||
if (!tokenStr.StartsWith("yi-"))
|
||||
{
|
||||
throw new UserFriendlyException("当前请求token非法", "401");
|
||||
}
|
||||
entity = await _tokenRepository._DbQueryable
|
||||
.Where(x => x.Token == tokenStr)
|
||||
.FirstAsync();
|
||||
}
|
||||
|
||||
if (entity is null)
|
||||
{
|
||||
throw new UserFriendlyException("当前请求token无效", "401");
|
||||
@@ -76,21 +96,28 @@ public class TokenManager : DomainService
|
||||
}
|
||||
|
||||
// 如果是尊享模型且Token设置了额度限制,检查是否超限
|
||||
if (!string.IsNullOrEmpty(modelId) &&
|
||||
PremiumPackageConst.ModeIds.Contains(modelId) &&
|
||||
entity.PremiumQuotaLimit.HasValue)
|
||||
if (!string.IsNullOrEmpty(modelId) && entity.PremiumQuotaLimit.HasValue)
|
||||
{
|
||||
var usedQuota = await GetTokenPremiumUsedQuotaAsync(entity.UserId, entity.Id);
|
||||
if (usedQuota >= entity.PremiumQuotaLimit.Value)
|
||||
var isPremium = await _aiModelRepository._DbQueryable
|
||||
.Where(x => x.ModelId == modelId)
|
||||
.Select(x => x.IsPremium)
|
||||
.FirstAsync();
|
||||
|
||||
if (isPremium)
|
||||
{
|
||||
throw new UserFriendlyException($"当前Token的尊享包额度已用完(已使用:{usedQuota},限制:{entity.PremiumQuotaLimit.Value}),请调整额度限制或使用其他Token", "403");
|
||||
var usedQuota = await GetTokenPremiumUsedQuotaAsync(entity.UserId, entity.Id);
|
||||
if (usedQuota >= entity.PremiumQuotaLimit.Value)
|
||||
{
|
||||
throw new UserFriendlyException($"当前Token的尊享包额度已用完(已使用:{usedQuota},限制:{entity.PremiumQuotaLimit.Value}),请调整额度限制或使用其他Token", "403");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new TokenValidationResult
|
||||
{
|
||||
UserId = entity.UserId,
|
||||
TokenId = entity.Id
|
||||
TokenId = entity.Id,
|
||||
Token = entity.Token
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,7 +126,11 @@ public class TokenManager : DomainService
|
||||
/// </summary>
|
||||
private async Task<long> GetTokenPremiumUsedQuotaAsync(Guid userId, Guid tokenId)
|
||||
{
|
||||
var premiumModelIds = PremiumPackageConst.ModeIds;
|
||||
// 先获取所有尊享模型的ModelId列表
|
||||
var premiumModelIds = await _aiModelRepository._DbQueryable
|
||||
.Where(x => x.IsPremium)
|
||||
.Select(x => x.ModelId)
|
||||
.ToListAsync();
|
||||
|
||||
var usedQuota = await _usageStatisticsRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.TokenId == tokenId && premiumModelIds.Contains(x.ModelId))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\common.props" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenAI" Version="2.8.0" />
|
||||
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
||||
<PackageReference Include="Volo.Abp.Caching" Version="$(AbpVersion)" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.57.0" />
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenAI" Version="2.8.0" />
|
||||
<PackageReference Include="Volo.Abp.AspNetCore.SignalR" Version="$(AbpVersion)" />
|
||||
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
||||
<PackageReference Include="Volo.Abp.Caching" Version="$(AbpVersion)" />
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Yi.Abp.Web.Jobs.ai_stock
|
||||
|
||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
return;
|
||||
// 每次触发只有2/24的概率执行生成新闻
|
||||
var random = new Random();
|
||||
var probability = random.Next(0, 24);
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Yi.Abp.Web.Jobs.ai_stock
|
||||
|
||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
return;
|
||||
await _stockMarketManager.GenerateStocksAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public class AutoPassInGoodsJob: HangfireBackgroundWorkerBase
|
||||
}
|
||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
return;
|
||||
await _marketManager.AutoPassInGoodsAsync();
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ public class AutoRefreshMiningPoolJob : HangfireBackgroundWorkerBase
|
||||
}
|
||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
|
||||
return;
|
||||
//刷新矿池
|
||||
await _miningPoolManager.RefreshMiningPoolAsync();
|
||||
//刷新用户限制
|
||||
|
||||
@@ -20,6 +20,7 @@ public class AutoUpdateCollectiblesValueJob : HangfireBackgroundWorkerBase
|
||||
|
||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
return;
|
||||
await _collectiblesManager.UpdateAllValueAsync();
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public class OnHookAutoMiningJob : HangfireBackgroundWorkerBase
|
||||
}
|
||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
return;
|
||||
await _miningPoolManager.OnHookMiningAsync();
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ namespace Yi.Abp.Web
|
||||
//本地开发环境,可以禁用作业执行
|
||||
if (host.IsDevelopment())
|
||||
{
|
||||
Configure<AbpBackgroundWorkerOptions>(options => { options.IsEnabled = false; });
|
||||
//Configure<AbpBackgroundWorkerOptions>(options => { options.IsEnabled = false; });
|
||||
}
|
||||
|
||||
//请求日志
|
||||
@@ -280,6 +280,7 @@ namespace Yi.Abp.Web
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
RoleClaimType = "Roles",
|
||||
ClockSkew = TimeSpan.Zero,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = jwtOptions.Issuer,
|
||||
@@ -298,7 +299,8 @@ namespace Yi.Abp.Web
|
||||
}
|
||||
else
|
||||
{
|
||||
if (messageContext.Request.Cookies.TryGetValue("Token", out var cookiesToken))
|
||||
if (!messageContext.Request.Headers.ContainsKey("Authorization") &&
|
||||
messageContext.Request.Cookies.TryGetValue("Token", out var cookiesToken))
|
||||
{
|
||||
messageContext.Token = cookiesToken;
|
||||
}
|
||||
@@ -358,8 +360,8 @@ namespace Yi.Abp.Web
|
||||
var app = context.GetApplicationBuilder();
|
||||
app.UseRouting();
|
||||
|
||||
//app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<AgentStoreAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeRecordAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ImageStoreTaskAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeRecordAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<UsageStatisticsAggregateRoot>();
|
||||
|
||||
//跨域
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npx vue-tsc --noEmit)",
|
||||
"Bash(timeout 60 npx vue-tsc:*)"
|
||||
"Bash(timeout 60 npx vue-tsc:*)",
|
||||
"Bash(npm run dev:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<body>
|
||||
<!-- 加载动画容器 -->
|
||||
<div id="yixinai-loader" class="loader-container">
|
||||
<div class="loader-title">意心Ai 2.9</div>
|
||||
<div class="loader-title">意心Ai 3.0</div>
|
||||
<div class="loader-subtitle">海外地址,仅首次访问预计加载约10秒,无需梯子</div>
|
||||
<div class="loader-logo">
|
||||
<div class="pulse-box"></div>
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
<!-- 全局样式 -->
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style>
|
||||
.popover-content
|
||||
{
|
||||
z-index: 99;
|
||||
}
|
||||
</style>
|
||||
|
||||
33
Yi.Ai.Vue3/src/api/aiImage/index.ts
Normal file
33
Yi.Ai.Vue3/src/api/aiImage/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { get, post } from '@/utils/request';
|
||||
import type {
|
||||
GenerateImageRequest,
|
||||
ImageModel,
|
||||
PublishImageRequest,
|
||||
TaskListRequest,
|
||||
TaskListResponse,
|
||||
TaskStatusResponse,
|
||||
} from './types';
|
||||
|
||||
export function generateImage(data: GenerateImageRequest) {
|
||||
return post<string>('/ai-image/generate', data).json();
|
||||
}
|
||||
|
||||
export function getTaskStatus(taskId: string) {
|
||||
return get<TaskStatusResponse>(`/ai-image/task/${taskId}`).json();
|
||||
}
|
||||
|
||||
export function getMyTasks(params: TaskListRequest) {
|
||||
return get<TaskListResponse>('/ai-image/my-tasks', params).json();
|
||||
}
|
||||
|
||||
export function getImagePlaza(params: TaskListRequest) {
|
||||
return get<TaskListResponse>('/ai-image/plaza', params).json();
|
||||
}
|
||||
|
||||
export function publishImage(data: PublishImageRequest) {
|
||||
return post<void>('/ai-image/publish', data).json();
|
||||
}
|
||||
|
||||
export function getImageModels() {
|
||||
return post<ImageModel[]>('/ai-image/model').json();
|
||||
}
|
||||
69
Yi.Ai.Vue3/src/api/aiImage/types.ts
Normal file
69
Yi.Ai.Vue3/src/api/aiImage/types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export interface GenerateImageRequest {
|
||||
tokenId: string;
|
||||
prompt: string;
|
||||
modelId: string;
|
||||
referenceImagesPrefixBase64?: string[];
|
||||
}
|
||||
|
||||
export interface TaskStatusResponse {
|
||||
id: string;
|
||||
prompt: string;
|
||||
storePrefixBase64?: string;
|
||||
storeUrl?: string;
|
||||
taskStatus: 'Processing' | 'Success' | 'Fail';
|
||||
publishStatus: string;
|
||||
categories: string[];
|
||||
creationTime: string;
|
||||
errorInfo?: string;
|
||||
}
|
||||
|
||||
export interface TaskListRequest {
|
||||
SkipCount: number;
|
||||
MaxResultCount: number;
|
||||
TaskStatus?: 'Processing' | 'Success' | 'Fail';
|
||||
Prompt?: string;
|
||||
PublishStatus?: 'Unpublished' | 'Published';
|
||||
StartTime?: string;
|
||||
EndTime?: string;
|
||||
OrderByColumn?: string;
|
||||
IsAsc?: string;
|
||||
IsAscending?: boolean;
|
||||
Sorting?: string;
|
||||
Categories?: string;
|
||||
UserName?: string;
|
||||
}
|
||||
|
||||
export interface TaskItem {
|
||||
id: string;
|
||||
prompt: string;
|
||||
storePrefixBase64?: string;
|
||||
storeUrl?: string;
|
||||
taskStatus: 'Processing' | 'Success' | 'Fail';
|
||||
publishStatus: string;
|
||||
categories: string[];
|
||||
creationTime: string;
|
||||
errorInfo?: string;
|
||||
isAnonymous?: boolean;
|
||||
userName?: string | null;
|
||||
userId?: string | null;
|
||||
}
|
||||
|
||||
export interface TaskListResponse {
|
||||
total: number;
|
||||
items: TaskItem[];
|
||||
}
|
||||
|
||||
export interface PublishImageRequest {
|
||||
taskId: string;
|
||||
categories: string[];
|
||||
isAnonymous?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageModel {
|
||||
id: string;
|
||||
modelId: string;
|
||||
modelName: string;
|
||||
modelDescribe: string;
|
||||
remark: string;
|
||||
isPremiumPackage: boolean;
|
||||
}
|
||||
100
Yi.Ai.Vue3/src/api/channel/index.ts
Normal file
100
Yi.Ai.Vue3/src/api/channel/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { del, get, post, put } from '@/utils/request';
|
||||
import type {
|
||||
AiAppDto,
|
||||
AiAppCreateInput,
|
||||
AiAppUpdateInput,
|
||||
AiAppGetListInput,
|
||||
AiModelDto,
|
||||
AiModelCreateInput,
|
||||
AiModelUpdateInput,
|
||||
AiModelGetListInput,
|
||||
PagedResultDto,
|
||||
} from './types';
|
||||
|
||||
// ==================== AI应用管理 ====================
|
||||
|
||||
// 获取AI应用列表
|
||||
export function getAppList(params?: AiAppGetListInput) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.searchKey) {
|
||||
queryParams.append('SearchKey', params.searchKey);
|
||||
}
|
||||
if (params?.skipCount !== undefined) {
|
||||
queryParams.append('SkipCount', params.skipCount.toString());
|
||||
}
|
||||
if (params?.maxResultCount !== undefined) {
|
||||
queryParams.append('MaxResultCount', params.maxResultCount.toString());
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const url = queryString ? `/channel/app?${queryString}` : '/channel/app';
|
||||
|
||||
return get<PagedResultDto<AiAppDto>>(url).json();
|
||||
}
|
||||
|
||||
// 根据ID获取AI应用
|
||||
export function getAppById(id: string) {
|
||||
return get<AiAppDto>(`/channel/app/${id}`).json();
|
||||
}
|
||||
|
||||
// 创建AI应用
|
||||
export function createApp(data: AiAppCreateInput) {
|
||||
return post<AiAppDto>('/channel/app', data).json();
|
||||
}
|
||||
|
||||
// 更新AI应用
|
||||
export function updateApp(data: AiAppUpdateInput) {
|
||||
return put<AiAppDto>('/channel/app', data).json();
|
||||
}
|
||||
|
||||
// 删除AI应用
|
||||
export function deleteApp(id: string) {
|
||||
return del(`/channel/app/${id}`).json();
|
||||
}
|
||||
|
||||
// ==================== AI模型管理 ====================
|
||||
|
||||
// 获取AI模型列表
|
||||
export function getModelList(params?: AiModelGetListInput) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.searchKey) {
|
||||
queryParams.append('SearchKey', params.searchKey);
|
||||
}
|
||||
if (params?.aiAppId) {
|
||||
queryParams.append('AiAppId', params.aiAppId);
|
||||
}
|
||||
if (params?.isPremiumOnly !== undefined) {
|
||||
queryParams.append('IsPremiumOnly', params.isPremiumOnly.toString());
|
||||
}
|
||||
if (params?.skipCount !== undefined) {
|
||||
queryParams.append('SkipCount', params.skipCount.toString());
|
||||
}
|
||||
if (params?.maxResultCount !== undefined) {
|
||||
queryParams.append('MaxResultCount', params.maxResultCount.toString());
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const url = queryString ? `/channel/model?${queryString}` : '/channel/model';
|
||||
|
||||
return get<PagedResultDto<AiModelDto>>(url).json();
|
||||
}
|
||||
|
||||
// 根据ID获取AI模型
|
||||
export function getModelById(id: string) {
|
||||
return get<AiModelDto>(`/channel/model/${id}`).json();
|
||||
}
|
||||
|
||||
// 创建AI模型
|
||||
export function createModel(data: AiModelCreateInput) {
|
||||
return post<AiModelDto>('/channel/model', data).json();
|
||||
}
|
||||
|
||||
// 更新AI模型
|
||||
export function updateModel(data: AiModelUpdateInput) {
|
||||
return put<AiModelDto>('/channel/model', data).json();
|
||||
}
|
||||
|
||||
// 删除AI模型
|
||||
export function deleteModel(id: string) {
|
||||
return del(`/channel/model/${id}`).json();
|
||||
}
|
||||
121
Yi.Ai.Vue3/src/api/channel/types.ts
Normal file
121
Yi.Ai.Vue3/src/api/channel/types.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
// 模型类型枚举
|
||||
export enum ModelTypeEnum {
|
||||
Chat = 0,
|
||||
Image = 1,
|
||||
Embedding = 2,
|
||||
PremiumChat = 3,
|
||||
}
|
||||
|
||||
// 模型API类型枚举
|
||||
export enum ModelApiTypeEnum {
|
||||
OpenAi = 0,
|
||||
Claude = 1,
|
||||
}
|
||||
|
||||
// AI应用DTO
|
||||
export interface AiAppDto {
|
||||
id: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
extraUrl?: string;
|
||||
apiKey: string;
|
||||
orderNum: number;
|
||||
creationTime: string;
|
||||
}
|
||||
|
||||
// 创建AI应用输入
|
||||
export interface AiAppCreateInput {
|
||||
name: string;
|
||||
endpoint: string;
|
||||
extraUrl?: string;
|
||||
apiKey: string;
|
||||
orderNum: number;
|
||||
}
|
||||
|
||||
// 更新AI应用输入
|
||||
export interface AiAppUpdateInput {
|
||||
id: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
extraUrl?: string;
|
||||
apiKey: string;
|
||||
orderNum: number;
|
||||
}
|
||||
|
||||
// 获取AI应用列表输入
|
||||
export interface AiAppGetListInput {
|
||||
searchKey?: string;
|
||||
skipCount?: number;
|
||||
maxResultCount?: number;
|
||||
}
|
||||
|
||||
// AI模型DTO
|
||||
export interface AiModelDto {
|
||||
id: string;
|
||||
handlerName: string;
|
||||
modelId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
orderNum: number;
|
||||
aiAppId: string;
|
||||
extraInfo?: string;
|
||||
modelType: ModelTypeEnum;
|
||||
modelApiType: ModelApiTypeEnum;
|
||||
multiplier: number;
|
||||
multiplierShow: number;
|
||||
providerName?: string;
|
||||
iconUrl?: string;
|
||||
isPremium: boolean;
|
||||
}
|
||||
|
||||
// 创建AI模型输入
|
||||
export interface AiModelCreateInput {
|
||||
handlerName: string;
|
||||
modelId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
orderNum: number;
|
||||
aiAppId: string;
|
||||
extraInfo?: string;
|
||||
modelType: ModelTypeEnum;
|
||||
modelApiType: ModelApiTypeEnum;
|
||||
multiplier: number;
|
||||
multiplierShow: number;
|
||||
providerName?: string;
|
||||
iconUrl?: string;
|
||||
isPremium: boolean;
|
||||
}
|
||||
|
||||
// 更新AI模型输入
|
||||
export interface AiModelUpdateInput {
|
||||
id: string;
|
||||
handlerName: string;
|
||||
modelId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
orderNum: number;
|
||||
aiAppId: string;
|
||||
extraInfo?: string;
|
||||
modelType: ModelTypeEnum;
|
||||
modelApiType: ModelApiTypeEnum;
|
||||
multiplier: number;
|
||||
multiplierShow: number;
|
||||
providerName?: string;
|
||||
iconUrl?: string;
|
||||
isPremium: boolean;
|
||||
}
|
||||
|
||||
// 获取AI模型列表输入
|
||||
export interface AiModelGetListInput {
|
||||
searchKey?: string;
|
||||
aiAppId?: string;
|
||||
isPremiumOnly?: boolean;
|
||||
skipCount?: number;
|
||||
maxResultCount?: number;
|
||||
}
|
||||
|
||||
// 分页结果
|
||||
export interface PagedResultDto<T> {
|
||||
items: T[];
|
||||
totalCount: number;
|
||||
}
|
||||
@@ -6,3 +6,4 @@ export * from './model';
|
||||
export * from './pay';
|
||||
export * from './session';
|
||||
export * from './user';
|
||||
export * from './aiImage';
|
||||
|
||||
@@ -138,7 +138,8 @@ export function disableToken(id: string) {
|
||||
// 新增接口2
|
||||
// 获取可选择的token信息
|
||||
export function getSelectableTokenInfo() {
|
||||
return get<any>('/token/select-list').json();
|
||||
// return get<any>('/token/select-list').json();
|
||||
return get<any>('/token/select-list?includeDefault=false').json();
|
||||
}
|
||||
/*
|
||||
返回数据
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { GoodsItem } from '@/api/pay';
|
||||
import { CircleCheck, Loading } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { createOrder, getOrderStatus } from '@/api';
|
||||
import { getGoodsList, GoodsCategoryType } from '@/api/pay';
|
||||
import SupportModelList from '@/components/userPersonalCenter/components/SupportModelList.vue';
|
||||
@@ -305,8 +304,6 @@ async function checkPaymentStatus(outTradeNo: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function toggleDetails() {
|
||||
showDetails.value = !showDetails.value;
|
||||
}
|
||||
@@ -322,7 +319,8 @@ function onClose() {
|
||||
|
||||
function goToActivation() {
|
||||
close();
|
||||
userStore.openUserCenter('activationCode');
|
||||
// 使用 window.location 进行跳转,避免 router 注入问题
|
||||
window.location.href = '/console/activation';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -31,33 +31,33 @@ class Particle {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.hue = hue;
|
||||
|
||||
|
||||
// Explosive physics
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = Math.random() * 15 + 2;
|
||||
const speed = Math.random() * 15 + 2;
|
||||
this.vx = Math.cos(angle) * speed;
|
||||
this.vy = Math.sin(angle) * speed;
|
||||
|
||||
|
||||
this.alpha = 1;
|
||||
this.decay = Math.random() * 0.015 + 0.005;
|
||||
this.gravity = 0.05;
|
||||
this.friction = 0.96;
|
||||
|
||||
this.friction = 0.96;
|
||||
|
||||
this.size = Math.random() * 3 + 1;
|
||||
this.brightness = 50; // Standard brightness for white bg visibility (0-100% HSL L value)
|
||||
this.flicker = Math.random() > 0.5;
|
||||
this.flicker = Math.random() > 0.5;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.vx *= this.friction;
|
||||
this.vy *= this.friction;
|
||||
this.vy += this.gravity;
|
||||
|
||||
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
|
||||
this.alpha -= this.decay;
|
||||
this.hue += 0.5;
|
||||
this.hue += 0.5;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
@@ -65,9 +65,9 @@ class Particle {
|
||||
// On white background:
|
||||
// We want high saturation (100%) and medium lightness (50%) to make colors pop against white.
|
||||
// If lightness is too high (like 80-100), it fades into white.
|
||||
const lightness = this.flicker ? Math.random() * 20 + 40 : this.brightness;
|
||||
const lightness = this.flicker ? Math.random() * 20 + 40 : this.brightness;
|
||||
ctx.fillStyle = `hsla(${this.hue}, 100%, ${lightness}%, ${this.alpha})`;
|
||||
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
@@ -93,7 +93,7 @@ class Shockwave {
|
||||
}
|
||||
|
||||
update() {
|
||||
this.radius += 15;
|
||||
this.radius += 15;
|
||||
this.alpha -= 0.05;
|
||||
this.lineWidth -= 0.2;
|
||||
}
|
||||
@@ -132,10 +132,10 @@ function animate() {
|
||||
if (!ctx) return;
|
||||
|
||||
// Clear with transparent fade for trails on white
|
||||
// 'destination-out' erases content.
|
||||
// 'destination-out' erases content.
|
||||
// To leave a trail on a white background (canvas is transparent over white gradient):
|
||||
// We need to gently erase what's there.
|
||||
|
||||
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; // Alpha controls trail length
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
@@ -177,14 +177,14 @@ function triggerCelebration() {
|
||||
canvas.width = parent.clientWidth;
|
||||
canvas.height = parent.clientHeight;
|
||||
}
|
||||
|
||||
|
||||
const cx = canvas.width / 2;
|
||||
const cy = canvas.height / 2;
|
||||
|
||||
// 1. Initial Mega Explosion
|
||||
triggerShake();
|
||||
createExplosion(cx, cy, Math.random() * 360);
|
||||
|
||||
|
||||
// Start loop
|
||||
animate();
|
||||
|
||||
@@ -194,15 +194,15 @@ function triggerCelebration() {
|
||||
count++;
|
||||
const rx = cx + (Math.random() - 0.5) * canvas.width * 0.8;
|
||||
const ry = cy + (Math.random() - 0.5) * canvas.height * 0.8;
|
||||
|
||||
|
||||
createExplosion(rx, ry, Math.random() * 360);
|
||||
|
||||
if (count % 3 === 0) triggerShake();
|
||||
|
||||
if (count % 3 === 0) triggerShake();
|
||||
|
||||
if (count > 25) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 120);
|
||||
}, 120);
|
||||
}
|
||||
|
||||
async function handleRedeem() {
|
||||
@@ -221,7 +221,7 @@ async function handleRedeem() {
|
||||
duration: 3000,
|
||||
showClose: true,
|
||||
});
|
||||
activationCode.value = '';
|
||||
activationCode.value = '';
|
||||
} catch (error: any) {
|
||||
// console.error(error);
|
||||
} finally {
|
||||
@@ -238,13 +238,13 @@ onUnmounted(() => {
|
||||
<div ref="containerRef" class="activation-container" :class="{ 'shake-anim': isShaking }">
|
||||
<!-- Removed Dark overlay -->
|
||||
<canvas ref="canvasRef" class="fireworks-canvas"></canvas>
|
||||
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="gift-icon-wrapper">
|
||||
<div class="gift-box">🎁</div>
|
||||
<div class="gift-glow"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 class="title">激活码兑换</h2>
|
||||
<p class="subtitle">开启您的专属惊喜权益</p>
|
||||
|
||||
@@ -258,9 +258,9 @@ onUnmounted(() => {
|
||||
clearable
|
||||
@keyup.enter="handleRedeem"
|
||||
/>
|
||||
|
||||
<el-button
|
||||
class="redeem-btn"
|
||||
|
||||
<el-button
|
||||
class="redeem-btn"
|
||||
:loading="loading"
|
||||
@click="handleRedeem"
|
||||
>
|
||||
@@ -321,7 +321,7 @@ onUnmounted(() => {
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
z-index: 11;
|
||||
z-index: 11;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 40px;
|
||||
|
||||
@@ -408,7 +408,10 @@ async function handleFlipCard(record: CardFlipRecord) {
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
// 4. 移动克隆卡片到屏幕中心并放大(考虑边界限制)
|
||||
const scale = Math.min(1.8, window.innerWidth / rect.width * 0.6); // 动态计算缩放比例
|
||||
// 移动端使用更小的缩放比例
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const maxScale = isMobile ? 1.5 : 1.8;
|
||||
const scale = Math.min(maxScale, window.innerWidth / rect.width * (isMobile ? 0.5 : 0.6));
|
||||
const scaledWidth = rect.width * scale;
|
||||
const scaledHeight = rect.height * scale;
|
||||
|
||||
@@ -416,8 +419,8 @@ async function handleFlipCard(record: CardFlipRecord) {
|
||||
let centerX = window.innerWidth / 2;
|
||||
let centerY = window.innerHeight / 2;
|
||||
|
||||
// 边界检查:确保卡片完全在视口内(留20px边距)
|
||||
const margin = 20;
|
||||
// 边界检查:确保卡片完全在视口内(移动端留更多边距)
|
||||
const margin = isMobile ? 30 : 20;
|
||||
const minX = scaledWidth / 2 + margin;
|
||||
const maxX = window.innerWidth - scaledWidth / 2 - margin;
|
||||
const minY = scaledHeight / 2 + margin;
|
||||
@@ -1253,6 +1256,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -1277,15 +1285,36 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
.lucky-float-ball {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
/* left: 20px; */
|
||||
/* top: 20px; */
|
||||
transform: translateX(-50%);
|
||||
z-index: 999;
|
||||
bottom: 20px;
|
||||
transition: all 0.3s
|
||||
cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
bottom: 15px;
|
||||
|
||||
.lucky-circle {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.lucky-content .lucky-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.lucky-content .lucky-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.lucky-label {
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.lucky-full {
|
||||
@@ -1408,6 +1437,12 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
animation: slideIn 0.5s ease;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 8px 10px;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.compact-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1497,6 +1532,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.shuffle-text {
|
||||
@@ -1505,6 +1545,15 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 8px rgba(255, 215, 0, 0.6);
|
||||
animation: textPulse 1.5s ease-in-out infinite;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1515,8 +1564,13 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
max-width: 100%;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 6px;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// 洗牌阶段样式
|
||||
@@ -1683,6 +1737,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 9px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
@@ -1691,6 +1750,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// 系统logo样式(优化为居中圆形,更丰富的效果)
|
||||
@@ -1709,6 +1772,12 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
z-index: 3;
|
||||
filter: brightness(1.1);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
// 外层光晕效果
|
||||
&::before {
|
||||
content: '';
|
||||
@@ -1741,6 +1810,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-type-badge {
|
||||
@@ -1753,6 +1826,12 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 2;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-shine {
|
||||
@@ -1827,6 +1906,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
// logo水印样式
|
||||
.result-watermark {
|
||||
position: absolute;
|
||||
@@ -1869,6 +1952,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
z-index: 1;
|
||||
filter: drop-shadow(0 4px 8px rgba(255, 215, 0, 0.5));
|
||||
margin-bottom: 4px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-text {
|
||||
@@ -1883,6 +1970,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
letter-spacing: 2px;
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
// 文字外发光
|
||||
&::after {
|
||||
content: attr(data-text);
|
||||
@@ -1907,6 +1999,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
position: relative;
|
||||
filter: drop-shadow(0 3px 10px rgba(255, 215, 0, 0.6));
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 32px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
// 金色光效边框
|
||||
&::before {
|
||||
content: attr(data-amount);
|
||||
@@ -1932,6 +2029,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
margin-top: 4px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1953,6 +2055,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
margin-bottom: 6px;
|
||||
filter: drop-shadow(0 2px 6px rgba(147, 112, 219, 0.3));
|
||||
animation: gentleBounce 2s ease-in-out infinite;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-text {
|
||||
@@ -1964,6 +2070,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
margin: 8px 0;
|
||||
z-index: 1;
|
||||
letter-spacing: 1px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-tip {
|
||||
@@ -1972,6 +2082,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
z-index: 1;
|
||||
margin-top: 6px;
|
||||
font-weight: 500;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1994,6 +2108,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
z-index: 1;
|
||||
filter: drop-shadow(0 4px 12px rgba(255, 215, 0, 0.8));
|
||||
margin: 10px 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 42px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mystery-text {
|
||||
@@ -2004,6 +2123,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
z-index: 1;
|
||||
letter-spacing: 4px;
|
||||
margin: 8px 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 18px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.mystery-hint {
|
||||
@@ -2012,6 +2136,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
z-index: 1;
|
||||
letter-spacing: 2px;
|
||||
margin-top: 6px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.mystery-stars {
|
||||
@@ -2060,6 +2189,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.code-input {
|
||||
@@ -2078,10 +2212,19 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 12px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 20px 12px;
|
||||
}
|
||||
|
||||
.filled-icon {
|
||||
font-size: 48px;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 40px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.filled-text {
|
||||
@@ -2090,6 +2233,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2128,12 +2275,22 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.code-text {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
letter-spacing: 6px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 24px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2142,8 +2299,17 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2153,6 +2319,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
line-height: 1.5;
|
||||
margin: 0 0 12px 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
@@ -2199,12 +2369,20 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.share-preview-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.share-preview-content {
|
||||
@@ -2218,6 +2396,12 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
padding: 8px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
@@ -2276,9 +2460,17 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.double-icon {
|
||||
font-size: 64px;
|
||||
animation: bounce 1s infinite;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 52px;
|
||||
}
|
||||
}
|
||||
|
||||
.double-sparkle {
|
||||
@@ -2287,6 +2479,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
right: -10px;
|
||||
font-size: 32px;
|
||||
animation: spin 2s linear infinite;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2295,6 +2491,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.double-text {
|
||||
@@ -2303,10 +2504,19 @@ function getCardClass(record: CardFlipRecord): string[] {
|
||||
color: #606266;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import type { DailyTaskItem, DailyTaskStatusOutput } from '@/api/dailyTask/types';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getTodayTaskStatus, claimTaskReward } from '@/api/dailyTask';
|
||||
import type { DailyTaskStatusOutput, DailyTaskItem } from '@/api/dailyTask/types';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { claimTaskReward, getTodayTaskStatus } from '@/api/dailyTask';
|
||||
|
||||
const taskData = ref<DailyTaskStatusOutput | null>(null);
|
||||
const loading = ref(false);
|
||||
@@ -17,15 +17,18 @@ async function fetchTaskStatus() {
|
||||
try {
|
||||
const res = await getTodayTaskStatus();
|
||||
taskData.value = res.data;
|
||||
} catch (error: any) {
|
||||
}
|
||||
catch (error: any) {
|
||||
ElMessage.error(error?.message || '获取任务状态失败');
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClaim(task: DailyTaskItem) {
|
||||
if (task.status !== 1) return;
|
||||
if (task.status !== 1)
|
||||
return;
|
||||
|
||||
claiming.value[task.level] = true;
|
||||
try {
|
||||
@@ -34,9 +37,11 @@ async function handleClaim(task: DailyTaskItem) {
|
||||
|
||||
// 刷新任务状态
|
||||
await fetchTaskStatus();
|
||||
} catch (error: any) {
|
||||
}
|
||||
catch (error: any) {
|
||||
ElMessage.error(error?.message || '领取奖励失败');
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
claiming.value[task.level] = false;
|
||||
}
|
||||
}
|
||||
@@ -76,8 +81,10 @@ function getButtonClass(task: DailyTaskItem): string {
|
||||
|
||||
// 获取进度条颜色
|
||||
function getProgressColor(task: DailyTaskItem): string {
|
||||
if (task.status === 2) return '#FFD700'; // 已完成:金色
|
||||
if (task.status === 1) return '#67C23A'; // 可领取:绿色
|
||||
if (task.status === 2)
|
||||
return '#FFD700'; // 已完成:金色
|
||||
if (task.status === 1)
|
||||
return '#67C23A'; // 可领取:绿色
|
||||
return '#409EFF'; // 进行中:蓝色
|
||||
}
|
||||
</script>
|
||||
@@ -86,15 +93,21 @@ function getProgressColor(task: DailyTaskItem): string {
|
||||
<div v-loading="loading" class="daily-task-container">
|
||||
<div class="task-header">
|
||||
<h2>每日任务</h2>
|
||||
<p class="task-desc">完成每日任务,领取额外尊享包 Token 奖励,可累加重复</p>
|
||||
<p class="task-desc">
|
||||
完成每日任务,领取额外尊享包 Token 奖励,可累加重复
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="taskData" class="task-content">
|
||||
<!-- 今日消耗统计 -->
|
||||
<div class="consumption-card">
|
||||
<div class="consumption-icon">🔥</div>
|
||||
<div class="consumption-icon">
|
||||
🔥
|
||||
</div>
|
||||
<div class="consumption-info">
|
||||
<div class="consumption-label">今日尊享包消耗</div>
|
||||
<div class="consumption-label">
|
||||
今日尊享包消耗
|
||||
</div>
|
||||
<div class="consumption-value">
|
||||
{{ formatTokenDisplay(taskData.todayConsumedTokens) }} Tokens
|
||||
</div>
|
||||
@@ -109,7 +122,7 @@ function getProgressColor(task: DailyTaskItem): string {
|
||||
class="task-item"
|
||||
:class="{
|
||||
'task-completed': task.status === 2,
|
||||
'task-claimable': task.status === 1
|
||||
'task-claimable': task.status === 1,
|
||||
}"
|
||||
>
|
||||
<div class="task-icon">
|
||||
@@ -187,7 +200,6 @@ function getProgressColor(task: DailyTaskItem): string {
|
||||
|
||||
<style scoped>
|
||||
.daily-task-container {
|
||||
padding: 20px;
|
||||
min-height: 400px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -83,10 +83,9 @@ onMounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.premium-service {
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%);
|
||||
//background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%);
|
||||
|
||||
// 美化滚动条
|
||||
&::-webkit-scrollbar {
|
||||
@@ -127,7 +126,6 @@ onMounted(() => {
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 768px) {
|
||||
.premium-service {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.usage-list-wrapper {
|
||||
|
||||
@@ -549,6 +549,7 @@ onBeforeUnmount(() => {
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button
|
||||
v-if="false"
|
||||
:icon="FullScreen"
|
||||
circle
|
||||
plain
|
||||
@@ -598,18 +599,12 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.usage-statistics {
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.usage-statistics:hover {
|
||||
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.usage-statistics.fullscreen-mode {
|
||||
@@ -619,7 +614,6 @@ onBeforeUnmount(() => {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2000;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
border-radius: 0;
|
||||
@@ -631,7 +625,6 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
@@ -655,7 +648,6 @@ onBeforeUnmount(() => {
|
||||
|
||||
.option-label {
|
||||
text-decoration: line-through;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,7 +662,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
&.disabled-icon {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,14 +700,10 @@ onBeforeUnmount(() => {
|
||||
.chart-card {
|
||||
margin-bottom: 30px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.chart-card:hover {
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,16 +105,15 @@ function bindWechat() {
|
||||
|
||||
<template>
|
||||
<div class="user-profile">
|
||||
<!-- 顶部标题 -->
|
||||
<div class="header">
|
||||
<h2>
|
||||
<el-icon><User /></el-icon>
|
||||
个人信息
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- 用户卡片 -->
|
||||
<el-card class="profile-card" shadow="hover">
|
||||
<!-- 顶部标题 -->
|
||||
<div class="header">
|
||||
<h2>
|
||||
<el-icon><User /></el-icon>
|
||||
个人信息
|
||||
</h2>
|
||||
</div>
|
||||
<!-- 头像和基本信息区域 -->
|
||||
<div class="user-header-section">
|
||||
<!-- 头像区域 -->
|
||||
@@ -138,7 +137,9 @@ function bindWechat() {
|
||||
|
||||
<!-- 用户名称和状态 -->
|
||||
<div class="user-info-quick">
|
||||
<h3 class="user-name">{{ userNick }}</h3>
|
||||
<h3 class="user-name">
|
||||
{{ userNick }}
|
||||
</h3>
|
||||
<div class="user-tags">
|
||||
<el-tag v-if="userVipStatus" type="warning" effect="dark" size="large">
|
||||
<el-icon><Promotion /></el-icon>
|
||||
@@ -153,8 +154,12 @@ function bindWechat() {
|
||||
</div>
|
||||
<div class="user-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ formatDate(user.creationTime)?.split(' ')[0] || '-' }}</div>
|
||||
<div class="stat-label">注册时间</div>
|
||||
<div class="stat-value">
|
||||
{{ formatDate(user.creationTime)?.split(' ')[0] || '-' }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
注册时间
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +176,9 @@ function bindWechat() {
|
||||
<el-icon><User /></el-icon>
|
||||
用户名
|
||||
</div>
|
||||
<div class="info-value">{{ user.userName || '-' }}</div>
|
||||
<div class="info-value">
|
||||
{{ user.userName || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 昵称 -->
|
||||
@@ -180,7 +187,9 @@ function bindWechat() {
|
||||
<el-icon><Postcard /></el-icon>
|
||||
昵称
|
||||
</div>
|
||||
<div class="info-value">{{ userNick }}</div>
|
||||
<div class="info-value">
|
||||
{{ userNick }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
@@ -215,7 +224,9 @@ function bindWechat() {
|
||||
<!-- 微信绑定 -->
|
||||
<div class="info-item full-width">
|
||||
<div class="info-label">
|
||||
<el-icon color="#07C160"><ChatDotRound /></el-icon>
|
||||
<el-icon color="#07C160">
|
||||
<ChatDotRound />
|
||||
</el-icon>
|
||||
微信绑定
|
||||
</div>
|
||||
<div class="info-value wechat-binding">
|
||||
@@ -292,7 +303,6 @@ function bindWechat() {
|
||||
|
||||
<style scoped>
|
||||
.user-profile {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type LayoutType = | 'vertical' | 'blankPage';
|
||||
export type LayoutType = 'default' | 'vertical' | 'blankPage' | 'blankPage';
|
||||
|
||||
// 仿豆包折叠逻辑
|
||||
export type CollapseType
|
||||
@@ -25,7 +25,7 @@ export interface DesignConfigState {
|
||||
// 是否折叠菜单
|
||||
isCollapse: boolean;
|
||||
// 安全区是否被悬停
|
||||
isSafeAreaHover: boolean;
|
||||
isCollapseConversationList: boolean;
|
||||
// 跟踪是否首次激活悬停
|
||||
hasActivatedHover: boolean;
|
||||
}
|
||||
@@ -65,15 +65,13 @@ const design: DesignConfigState = {
|
||||
// 需要自定义路由动画可以把 Main 组件样式代码注释放开,从新对话切换到带id的路由时,会执行这个动画样式
|
||||
pageAnimateType: 'zoom-fade',
|
||||
// 布局模式 (纵向:vertical | ... | 自己定义)
|
||||
layout: 'vertical',
|
||||
layout: 'default',
|
||||
// 折叠类型
|
||||
collapseType: 'followSystem',
|
||||
// 是否折叠菜单
|
||||
// 是否折叠对话记录菜单
|
||||
isCollapse: false,
|
||||
// 安全区是否被悬停
|
||||
isSafeAreaHover: false,
|
||||
// 跟踪是否首次激活悬停
|
||||
hasActivatedHover: false,
|
||||
// 是否折叠对话记录菜单
|
||||
isCollapseConversationList: false,
|
||||
};
|
||||
|
||||
export default design;
|
||||
|
||||
@@ -12,4 +12,4 @@ export const COLLAPSE_THRESHOLD: number = 600;
|
||||
export const SIDE_BAR_WIDTH: number = 280;
|
||||
|
||||
// 路由白名单地址[本地存在的路由 staticRouter.ts 中]
|
||||
export const ROUTER_WHITE_LIST: string[] = ['/chat', '/chat/not_login', '/products', '/model-library', '/403', '/404'];
|
||||
export const ROUTER_WHITE_LIST: string[] = ['/chat', '/chat/conversation', '/chat/image', '/chat/video', '/model-library', '/403', '/404'];
|
||||
|
||||
110
Yi.Ai.Vue3/src/config/permission.ts
Normal file
110
Yi.Ai.Vue3/src/config/permission.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 权限配置文件
|
||||
* 用于配置特定页面的访问权限
|
||||
*/
|
||||
|
||||
/**
|
||||
* 权限配置接口
|
||||
*/
|
||||
export interface PermissionConfig {
|
||||
/** 路由路径 */
|
||||
path: string;
|
||||
/** 允许访问的用户名列表 */
|
||||
allowedUsers: string[];
|
||||
/** 权限描述 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面权限配置列表
|
||||
* 在这里配置需要特殊权限控制的页面
|
||||
*/
|
||||
export const PAGE_PERMISSIONS: PermissionConfig[] = [
|
||||
{
|
||||
path: '/console/channel',
|
||||
allowedUsers: ['cc', 'Guo'],
|
||||
description: '渠道商管理页面 - 仅限cc和Guo用户访问',
|
||||
},
|
||||
// 可以在这里继续添加其他需要权限控制的页面
|
||||
// {
|
||||
// path: '/console/admin',
|
||||
// allowedUsers: ['admin', 'superadmin'],
|
||||
// description: '管理员页面',
|
||||
// },
|
||||
];
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限访问指定路径
|
||||
* @param path 路由路径
|
||||
* @param userName 用户名
|
||||
* @returns 是否有权限
|
||||
*/
|
||||
export function checkPagePermission(path: string, userName: string | undefined): boolean {
|
||||
// 如果没有用户名,返回false
|
||||
if (!userName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查找该路径的权限配置
|
||||
const permissionConfig = PAGE_PERMISSIONS.find(config => config.path === path);
|
||||
|
||||
// 如果没有配置权限,说明该页面不需要特殊权限,返回true
|
||||
if (!permissionConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查用户名是否在允许列表中(不区分大小写)
|
||||
return permissionConfig.allowedUsers.some(
|
||||
allowedUser => allowedUser.toLowerCase() === userName.toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户无权访问的路由列表
|
||||
* @param userName 用户名
|
||||
* @returns 无权访问的路由路径数组
|
||||
*/
|
||||
export function getRestrictedRoutes(userName: string | undefined): string[] {
|
||||
if (!userName) {
|
||||
return PAGE_PERMISSIONS.map(config => config.path);
|
||||
}
|
||||
|
||||
return PAGE_PERMISSIONS.filter(
|
||||
config => !config.allowedUsers.some(
|
||||
allowedUser => allowedUser.toLowerCase() === userName.toLowerCase(),
|
||||
),
|
||||
).map(config => config.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路由是否需要权限控制
|
||||
* @param path 路由路径
|
||||
* @returns 是否需要权限控制
|
||||
*/
|
||||
export function isRestrictedRoute(path: string): boolean {
|
||||
return PAGE_PERMISSIONS.some(config => config.path === path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤菜单路由,移除用户无权访问的菜单项
|
||||
* @param routes 路由配置数组
|
||||
* @param userName 用户名
|
||||
* @returns 过滤后的路由配置数组
|
||||
*/
|
||||
export function filterMenuRoutes(routes: any[], userName: string | undefined): any[] {
|
||||
return routes.filter((route) => {
|
||||
// 检查当前路由是否有权限
|
||||
const hasPermission = checkPagePermission(route.path, userName);
|
||||
|
||||
if (!hasPermission) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果有子路由,递归过滤
|
||||
if (route.children && route.children.length > 0) {
|
||||
route.children = filterMenuRoutes(route.children, userName);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
72
Yi.Ai.Vue3/src/hooks/useResponsive.ts
Normal file
72
Yi.Ai.Vue3/src/hooks/useResponsive.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useBreakpoints, useWindowSize } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 断点定义
|
||||
export const breakpoints = {
|
||||
xs: 0, // 手机竖屏 < 640px
|
||||
sm: 640, // 手机横屏 ≥ 640px
|
||||
md: 768, // 平板 ≥ 768px
|
||||
lg: 1024, // 小桌面 ≥ 1024px
|
||||
xl: 1280, // 桌面 ≥ 1280px
|
||||
xxl: 1536, // 大桌面 ≥ 1536px
|
||||
};
|
||||
|
||||
export function useResponsive() {
|
||||
const bp = useBreakpoints(breakpoints);
|
||||
|
||||
// 设备类型判断
|
||||
const isMobile = bp.smaller('md'); // < 768px
|
||||
const isTablet = bp.between('md', 'lg'); // 768px - 1024px
|
||||
const isDesktop = bp.greaterOrEqual('lg'); // ≥ 1024px
|
||||
|
||||
// 精确断点
|
||||
const isXs = bp.smaller('sm'); // < 640px
|
||||
const isSm = bp.between('sm', 'md'); // 640px - 768px
|
||||
const isMd = bp.between('md', 'lg'); // 768px - 1024px
|
||||
const isLg = bp.between('lg', 'xl'); // 1024px - 1280px
|
||||
const isXl = bp.between('xl', 'xxl'); // 1280px - 1536px
|
||||
const isXxl = bp.greater('xxl'); // > 1536px
|
||||
|
||||
// 监听窗口变化
|
||||
const { width, height } = useWindowSize();
|
||||
|
||||
// 方向检测
|
||||
const isPortrait = computed(() => height.value > width.value);
|
||||
const isLandscape = computed(() => width.value > height.value);
|
||||
|
||||
// 当前断点名称
|
||||
const currentBreakpoint = computed(() => {
|
||||
if (isXs.value) return 'xs';
|
||||
if (isSm.value) return 'sm';
|
||||
if (isMd.value) return 'md';
|
||||
if (isLg.value) return 'lg';
|
||||
if (isXl.value) return 'xl';
|
||||
return 'xxl';
|
||||
});
|
||||
|
||||
return {
|
||||
// 设备类型
|
||||
isMobile,
|
||||
isTablet,
|
||||
isDesktop,
|
||||
|
||||
// 精确断点
|
||||
isXs,
|
||||
isSm,
|
||||
isMd,
|
||||
isLg,
|
||||
isXl,
|
||||
isXxl,
|
||||
|
||||
// 尺寸
|
||||
width,
|
||||
height,
|
||||
|
||||
// 方向
|
||||
isPortrait,
|
||||
isLandscape,
|
||||
|
||||
// 当前断点名称
|
||||
currentBreakpoint,
|
||||
};
|
||||
}
|
||||
@@ -19,8 +19,9 @@ export function useWindowWidthObserver(
|
||||
const isAboveThreshold = ref(false);
|
||||
const thresholdRef = ref(threshold);
|
||||
let prevIsAbove = false; // 记录上一次状态,避免重复触发
|
||||
|
||||
// 待定 待梳理 1227
|
||||
// 默认逻辑:修改全局折叠状态
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const updateCollapseState = (isAbove: boolean) => {
|
||||
// 判断当前的折叠状态
|
||||
switch (designStore.collapseType) {
|
||||
@@ -70,7 +71,7 @@ export function useWindowWidthObserver(
|
||||
onChange(newIsAbove);
|
||||
}
|
||||
else {
|
||||
updateCollapseState(newIsAbove);
|
||||
// updateCollapseState(newIsAbove);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
35
Yi.Ai.Vue3/src/layouts/LayoutDefault/index.vue
Normal file
35
Yi.Ai.Vue3/src/layouts/LayoutDefault/index.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import SystemAnnouncementDialog from '@/components/SystemAnnouncementDialog/index.vue';
|
||||
import Header from '@/layouts/components/Header/index.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-header class="layout-header">
|
||||
<Header />
|
||||
</el-header>
|
||||
<el-container class="layout-container-main">
|
||||
<router-view />
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<!-- 系统公告弹窗 -->
|
||||
<SystemAnnouncementDialog />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--color-white);
|
||||
.layout-header {
|
||||
padding: 0;
|
||||
border-bottom: var(--header-border) ;
|
||||
}
|
||||
.layout-container-main {
|
||||
height: calc(100vh - var(--header-container-default-height));
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,288 @@
|
||||
<!-- 手机端布局 -->
|
||||
<script setup></script>
|
||||
<!-- 移动端布局 -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 侧边栏抽屉状态
|
||||
const drawerVisible = ref(false);
|
||||
|
||||
// 底部导航菜单
|
||||
const bottomMenus = [
|
||||
{
|
||||
key: 'chat',
|
||||
label: '对话',
|
||||
icon: 'ChatDotRound',
|
||||
path: '/chat/conversation',
|
||||
},
|
||||
{
|
||||
key: 'image',
|
||||
label: '图片',
|
||||
icon: 'Picture',
|
||||
path: '/chat/image',
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
label: '视频',
|
||||
icon: 'VideoCamera',
|
||||
path: '/chat/video',
|
||||
},
|
||||
{
|
||||
key: 'console',
|
||||
label: '我的',
|
||||
icon: 'User',
|
||||
path: '/console',
|
||||
},
|
||||
];
|
||||
|
||||
// 侧边栏菜单
|
||||
const sidebarMenus = [
|
||||
{
|
||||
key: 'model-library',
|
||||
label: '模型库',
|
||||
icon: 'Box',
|
||||
path: '/model-library',
|
||||
},
|
||||
{
|
||||
key: 'pricing',
|
||||
label: '购买',
|
||||
icon: 'ShoppingCart',
|
||||
path: '/pricing',
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
label: '退出登录',
|
||||
icon: 'SwitchButton',
|
||||
action: 'logout',
|
||||
},
|
||||
];
|
||||
|
||||
// 当前路由
|
||||
const currentPath = computed(() => router.currentRoute.value.path);
|
||||
|
||||
// 当前激活的底部菜单
|
||||
const activeBottomMenu = computed(() => {
|
||||
const path = currentPath.value;
|
||||
if (path.includes('/chat/conversation')) return 'chat';
|
||||
if (path.includes('/chat/image')) return 'image';
|
||||
if (path.includes('/chat/video')) return 'video';
|
||||
if (path.includes('/console')) return 'console';
|
||||
return 'chat';
|
||||
});
|
||||
|
||||
// 打开抽屉
|
||||
function openDrawer() {
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
// 底部菜单点击
|
||||
function handleBottomMenuClick(menu: typeof bottomMenus[0]) {
|
||||
router.push(menu.path);
|
||||
}
|
||||
|
||||
// 侧边栏菜单点击
|
||||
function handleSidebarMenuClick(menu: typeof sidebarMenus[0]) {
|
||||
if (menu.action === 'logout') {
|
||||
userStore.logout();
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
else if (menu.path) {
|
||||
router.push(menu.path);
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
<div class="mobile-layout">
|
||||
<!-- 顶部栏 -->
|
||||
<div class="mobile-header">
|
||||
<el-button circle @click="openDrawer">
|
||||
<el-icon><i-ep-menu /></el-icon>
|
||||
</el-button>
|
||||
<div class="header-title">意心AI</div>
|
||||
<div class="header-avatar">
|
||||
<el-avatar v-if="userStore.userInfo" :size="32" :src="userStore.userInfo.avatar">
|
||||
{{ userStore.userInfo.name?.charAt(0) }}
|
||||
</el-avatar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="mobile-main">
|
||||
<router-view />
|
||||
</div>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<div class="mobile-bottom-nav">
|
||||
<div
|
||||
v-for="menu in bottomMenus"
|
||||
:key="menu.key"
|
||||
class="nav-item"
|
||||
:class="{ active: activeBottomMenu === menu.key }"
|
||||
@click="handleBottomMenuClick(menu)"
|
||||
>
|
||||
<el-icon class="nav-icon">
|
||||
<component :is="`i-ep-${menu.icon}`" />
|
||||
</el-icon>
|
||||
<div class="nav-label">
|
||||
{{ menu.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏抽屉 -->
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
title="菜单"
|
||||
direction="ltr"
|
||||
size="280px"
|
||||
>
|
||||
<!-- 用户信息 -->
|
||||
<div v-if="userStore.userInfo" class="drawer-user">
|
||||
<el-avatar :size="60" :src="userStore.userInfo.avatar">
|
||||
{{ userStore.userInfo.name?.charAt(0) }}
|
||||
</el-avatar>
|
||||
<div class="user-info">
|
||||
<div class="user-name">{{ userStore.userInfo.name }}</div>
|
||||
<div class="user-email">{{ userStore.userInfo.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<el-menu class="drawer-menu">
|
||||
<el-menu-item
|
||||
v-for="menu in sidebarMenus"
|
||||
:key="menu.key"
|
||||
@click="handleSidebarMenuClick(menu)"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="`i-ep-${menu.icon}`" />
|
||||
</el-icon>
|
||||
<span>{{ menu.label }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.mobile-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.mobile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.header-avatar {
|
||||
width: 40px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mobile-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
height: 56px;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background-color: var(--el-bg-color);
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
color: var(--el-color-primary);
|
||||
|
||||
.nav-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 2px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 抽屉样式
|
||||
.drawer-user {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px 16px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.drawer-menu {
|
||||
border-right: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<script setup lang="ts">
|
||||
import SystemAnnouncementDialog from '@/components/SystemAnnouncementDialog/index.vue';
|
||||
import { useSafeArea } from '@/hooks/useSafeArea';
|
||||
import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
|
||||
import Aside from '@/layouts/components/Aside/index.vue';
|
||||
import Header from '@/layouts/components/Header/index.vue';
|
||||
import Main from '@/layouts/components/Main/index.vue';
|
||||
import { useAnnouncementStore, useDesignStore } from '@/stores';
|
||||
@@ -25,7 +23,7 @@ useSafeArea({
|
||||
});
|
||||
|
||||
/** 监听窗口大小变化,折叠侧边栏 */
|
||||
useWindowWidthObserver();
|
||||
// useWindowWidthObserver();
|
||||
|
||||
// 应用加载时检查是否需要显示公告弹窗
|
||||
onMounted(() => {
|
||||
@@ -43,7 +41,7 @@ onMounted(() => {
|
||||
<Header />
|
||||
</el-header>
|
||||
<el-container class="layout-container-main">
|
||||
<Aside />
|
||||
<!-- <Aside /> -->
|
||||
<el-main class="layout-main">
|
||||
<!-- 路由页面 -->
|
||||
<Main />
|
||||
@@ -55,29 +53,29 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
.layout-header {
|
||||
padding: 0;
|
||||
}
|
||||
.layout-main {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
.layout-container-main {
|
||||
margin-left: var(--sidebar-left-container-default-width, 0);
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/** 去除菜单右侧边框 */
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
.layout-scrollbar {
|
||||
width: 100%;
|
||||
}
|
||||
//.layout-container {
|
||||
// position: relative;
|
||||
// width: 100%;
|
||||
// height: 100vh;
|
||||
// overflow: hidden;
|
||||
// .layout-header {
|
||||
// padding: 0;
|
||||
// }
|
||||
// .layout-main {
|
||||
// height: 100%;
|
||||
// padding: 0;
|
||||
// }
|
||||
// .layout-container-main {
|
||||
// margin-left: var(--sidebar-left-container-default-width, 0);
|
||||
// transition: margin-left 0.3s ease;
|
||||
// }
|
||||
//}
|
||||
//
|
||||
///** 去除菜单右侧边框 */
|
||||
//.el-menu {
|
||||
// border-right: none;
|
||||
//}
|
||||
//.layout-scrollbar {
|
||||
// width: 100%;
|
||||
//}
|
||||
</style>
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
<!-- Aside 侧边栏 -->
|
||||
<script setup lang="ts">
|
||||
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations';
|
||||
import type { ChatSessionVo } from '@/api/session/types';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { get_session } from '@/api';
|
||||
import logo from '@/assets/images/logo.png';
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import Collapse from '@/layouts/components/Header/components/Collapse.vue';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const sessionId = computed(() => route.params?.id);
|
||||
const conversationsList = computed(() => sessionStore.sessionList);
|
||||
const loadMoreLoading = computed(() => sessionStore.isLoadingMore);
|
||||
const active = ref<string | undefined>();
|
||||
|
||||
onMounted(async () => {
|
||||
// 获取会话列表
|
||||
await sessionStore.requestSessionList();
|
||||
// 高亮最新会话
|
||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||
// 通过 ID 查询详情,设置当前会话 (因为有分页)
|
||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sessionStore.currentSession,
|
||||
(newValue) => {
|
||||
active.value = newValue ? `${newValue.id}` : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
// 创建会话
|
||||
function handleCreatChat() {
|
||||
// 创建会话, 跳转到默认聊天
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function handleChange(item: ConversationItem<ChatSessionVo>) {
|
||||
sessionStore.setCurrentSession(item);
|
||||
router.replace({
|
||||
name: 'chatWithId',
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 处理组件触发的加载更多事件
|
||||
async function handleLoadMore() {
|
||||
if (!sessionStore.hasMore)
|
||||
return; // 无更多数据时不加载
|
||||
await sessionStore.loadMoreSessions();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo>) {
|
||||
switch (command) {
|
||||
case 'delete':
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
// 删除会话
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
// 如果删除当前会话 返回到默认页
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
break;
|
||||
case 'rename':
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: item.sessionTitle, // 设置默认值
|
||||
autofocus: false,
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}).then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: item.id!,
|
||||
sessionTitle: value,
|
||||
sessionContent: item.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
// 如果是当前会话,则更新当前选中会话信息
|
||||
if (sessionStore.currentSession?.id === item.id) {
|
||||
sessionStore.setCurrentSession({
|
||||
...item,
|
||||
sessionTitle: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="aside-container"
|
||||
:class="{
|
||||
'aside-container-suspended': designStore.isSafeAreaHover,
|
||||
'aside-container-collapse': designStore.isCollapse,
|
||||
// 折叠且未激活悬停时添加 no-delay 类
|
||||
'no-delay': designStore.isCollapse && !designStore.hasActivatedHover,
|
||||
}"
|
||||
>
|
||||
<div class="aside-wrapper">
|
||||
<div v-if="!designStore.isCollapse" class="aside-header">
|
||||
<div class="flex items-center gap-8px hover:cursor-pointer" @click="handleCreatChat">
|
||||
<el-image :src="logo" alt="logo" fit="cover" class="logo-img" />
|
||||
<span class="logo-text max-w-150px text-overflow">意心AI</span>
|
||||
</div>
|
||||
<Collapse class="ml-auto" />
|
||||
</div>
|
||||
|
||||
<div class="aside-body">
|
||||
<div class="creat-chat-btn-wrapper">
|
||||
<div class="creat-chat-btn" @click="handleCreatChat">
|
||||
<el-icon class="add-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span class="creat-chat-text">新对话</span>
|
||||
<SvgIcon name="ctrl+k" size="37" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="aside-content">
|
||||
<div v-if="conversationsList.length > 0" class="conversations-wrap overflow-hidden">
|
||||
<Conversations
|
||||
v-model:active="active"
|
||||
:items="conversationsList"
|
||||
:label-max-width="200"
|
||||
:show-tooltip="true"
|
||||
:tooltip-offset="60"
|
||||
show-built-in-menu
|
||||
groupable
|
||||
row-key="id"
|
||||
label-key="sessionTitle"
|
||||
tooltip-placement="right"
|
||||
:load-more="handleLoadMore"
|
||||
:load-more-loading="loadMoreLoading"
|
||||
:items-style="{
|
||||
marginLeft: '8px',
|
||||
userSelect: 'none',
|
||||
borderRadius: '10px',
|
||||
padding: '8px 12px',
|
||||
}"
|
||||
:items-active-style="{
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
}"
|
||||
:items-hover-style="{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||
}"
|
||||
@menu-command="handleMenuCommand"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-else class="h-full flex-center" description="暂无对话记录" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 基础样式
|
||||
.aside-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
// z-index: 11;
|
||||
width: var(--sidebar-default-width);
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-right: 0.5px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
.aside-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
// 侧边栏头部样式
|
||||
.aside-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin: 10px 12px 0;
|
||||
.logo-img {
|
||||
box-sizing: border-box;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
img {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
transform: skewX(-2deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏内容样式
|
||||
.aside-body {
|
||||
.creat-chat-btn-wrapper {
|
||||
padding: 0 12px;
|
||||
.creat-chat-btn {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 8px 6px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
color: #0057ff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: rgb(0 87 255 / 6%);
|
||||
border: 1px solid rgb(0 102 255 / 15%);
|
||||
border-radius: 12px;
|
||||
&:hover {
|
||||
background-color: rgb(0 87 255 / 12%);
|
||||
}
|
||||
.creat-chat-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
}
|
||||
.add-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.svg-icon {
|
||||
height: 24px;
|
||||
margin-left: auto;
|
||||
color: rgb(0 87 255 / 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.aside-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
||||
// 会话列表高度-基础样式
|
||||
.conversations-wrap {
|
||||
height: calc(100vh - 110px);
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠样式
|
||||
.aside-container-collapse {
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
// z-index: 22;
|
||||
height: auto;
|
||||
max-height: calc(100% - 110px);
|
||||
padding-bottom: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
/* 禁用悬停事件 */
|
||||
pointer-events: none;
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
border-radius: 15px;
|
||||
box-shadow:
|
||||
0 10px 20px 0 rgb(0 0 0 / 10%),
|
||||
0 0 1px 0 rgb(0 0 0 / 15%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease 0.3s, transform 0.3s ease 0.3s;
|
||||
|
||||
// 指定样式过渡
|
||||
|
||||
// 向左偏移一个宽度
|
||||
transform: translateX(-100%);
|
||||
|
||||
/* 新增:未激活悬停时覆盖延迟 */
|
||||
&.no-delay {
|
||||
transition-delay: 0s, 0s;
|
||||
}
|
||||
}
|
||||
|
||||
// 悬停样式
|
||||
.aside-container-collapse:hover,
|
||||
.aside-container-collapse.aside-container-suspended {
|
||||
height: auto;
|
||||
max-height: calc(100% - 110px);
|
||||
padding-bottom: 12px;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
border-radius: 15px;
|
||||
box-shadow:
|
||||
0 10px 20px 0 rgb(0 0 0 / 10%),
|
||||
0 0 1px 0 rgb(0 0 0 / 15%);
|
||||
|
||||
// 直接在这里写悬停时的样式(与 aside-container-suspended 一致)
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease 0s, transform 0.3s ease 0s;
|
||||
|
||||
// 过渡动画沿用原有设置
|
||||
transform: translateX(15px);
|
||||
|
||||
// 会话列表高度-悬停样式
|
||||
.conversations-wrap {
|
||||
height: calc(100vh - 155px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 样式穿透
|
||||
:deep() {
|
||||
// 会话列表背景色
|
||||
.conversations-list {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
// 群组标题样式 和 侧边栏菜单背景色一致
|
||||
.conversation-group-title {
|
||||
padding-left: 12px !important;
|
||||
background-color: var(--sidebar-background-color) !important;
|
||||
}
|
||||
.conversation-group .active-sticky
|
||||
{
|
||||
z-index: 0 ;
|
||||
}
|
||||
.conversation-group .sticky-title{
|
||||
z-index: 0 ;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
804
Yi.Ai.Vue3/src/layouts/components/ChatAside/index.vue
Normal file
804
Yi.Ai.Vue3/src/layouts/components/ChatAside/index.vue
Normal file
@@ -0,0 +1,804 @@
|
||||
<script setup lang="ts">
|
||||
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations';
|
||||
import type { ChatSessionVo } from '@/api/session/types';
|
||||
import { ChatLineSquare, Expand, Fold, MoreFilled, Plus } from '@element-plus/icons-vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { get_session } from '@/api';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const sessionId = computed(() => route.params?.id);
|
||||
const conversationsList = computed(() => sessionStore.sessionList);
|
||||
const loadMoreLoading = computed(() => sessionStore.isLoadingMore);
|
||||
const active = ref<string | undefined>();
|
||||
const isCollapsed = computed(() => designStore.isCollapseConversationList);
|
||||
|
||||
// 判断是否为新建对话状态(没有选中任何会话)
|
||||
const isNewChatState = computed(() => !sessionStore.currentSession);
|
||||
|
||||
onMounted(async () => {
|
||||
await sessionStore.requestSessionList();
|
||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sessionStore.currentSession,
|
||||
(newValue) => {
|
||||
active.value = newValue ? `${newValue.id}` : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
// 创建会话
|
||||
function handleCreatChat() {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function handleChange(item: ConversationItem<ChatSessionVo>) {
|
||||
sessionStore.setCurrentSession(item);
|
||||
router.replace({
|
||||
name: 'chatConversationWithId',
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 处理组件触发的加载更多事件
|
||||
async function handleLoadMore() {
|
||||
if (!sessionStore.hasMore)
|
||||
return;
|
||||
await sessionStore.loadMoreSessions();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo>) {
|
||||
switch (command) {
|
||||
case 'delete':
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
break;
|
||||
case 'rename':
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: item.sessionTitle,
|
||||
autofocus: false,
|
||||
inputValidator: (value) => {
|
||||
return !!value;
|
||||
},
|
||||
}).then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: item.id!,
|
||||
sessionTitle: value,
|
||||
sessionContent: item.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
if (sessionStore.currentSession?.id === item.id) {
|
||||
sessionStore.setCurrentSession({
|
||||
...item,
|
||||
sessionTitle: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠/展开侧边栏
|
||||
function toggleSidebar() {
|
||||
designStore.setIsCollapseConversationList(!designStore.isCollapseConversationList);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="aside-container"
|
||||
:class="{ 'aside-collapsed': isCollapsed }"
|
||||
>
|
||||
<div class="aside-wrapper">
|
||||
<!-- 头部 -->
|
||||
<div class="aside-header">
|
||||
<!-- 展开状态显示logo和标题 -->
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
class="header-content-expanded flex items-center gap-8px"
|
||||
:class="{ 'is-disabled': isNewChatState, 'hover:cursor-pointer': !isNewChatState }"
|
||||
@click="!isNewChatState && handleCreatChat()"
|
||||
>
|
||||
<span class="logo-text max-w-150px text-overflow">会话</span>
|
||||
</div>
|
||||
|
||||
<!-- 折叠状态只显示logo -->
|
||||
<div
|
||||
v-else
|
||||
class="header-content-collapsed flex items-center justify-center hover:cursor-pointer"
|
||||
>
|
||||
<el-icon size="20">
|
||||
<ChatLineSquare />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 折叠按钮 -->
|
||||
<el-tooltip
|
||||
:content="isCollapsed ? '展开侧边栏' : '折叠侧边栏'"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button
|
||||
class="collapse-btn"
|
||||
type="text"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<el-icon v-if="isCollapsed">
|
||||
<Expand />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Fold />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="aside-body">
|
||||
<!-- 创建会话按钮 -->
|
||||
<div class="creat-chat-btn-wrapper">
|
||||
<div
|
||||
class="creat-chat-btn"
|
||||
:class="{
|
||||
'creat-chat-btn-collapsed': isCollapsed,
|
||||
'is-disabled': isNewChatState,
|
||||
}"
|
||||
@click="!isNewChatState && handleCreatChat()"
|
||||
>
|
||||
<el-icon class="add-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span v-if="!isCollapsed" class="creat-chat-text">
|
||||
新对话
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
<div class="aside-content">
|
||||
<div v-if="conversationsList.length > 0" class="conversations-wrap">
|
||||
<Conversations
|
||||
v-model:active="active"
|
||||
:items="conversationsList"
|
||||
:label-max-width="isCollapsed ? 0 : 140"
|
||||
:show-tooltip="!isCollapsed"
|
||||
:tooltip-offset="60"
|
||||
show-built-in-menu
|
||||
groupable
|
||||
row-key="id"
|
||||
label-key="sessionTitle"
|
||||
:tooltip-placement="isCollapsed ? 'right-start' : 'right'"
|
||||
:load-more="handleLoadMore"
|
||||
:load-more-loading="loadMoreLoading"
|
||||
:items-style="{
|
||||
marginLeft: '8px',
|
||||
marginRight: '8px',
|
||||
userSelect: 'none',
|
||||
borderRadius: isCollapsed ? '12px' : '10px',
|
||||
padding: isCollapsed ? '12px 8px' : '8px 12px',
|
||||
justifyContent: isCollapsed ? 'center' : 'space-between',
|
||||
width: isCollapsed ? '64px' : 'auto',
|
||||
height: isCollapsed ? '64px' : 'auto',
|
||||
minHeight: '48px',
|
||||
flexDirection: isCollapsed ? 'column' : 'row',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
:items-active-style="{
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
}"
|
||||
:items-hover-style="{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||
}"
|
||||
@menu-command="handleMenuCommand"
|
||||
@change="handleChange"
|
||||
@contextmenu="handleContextMenu"
|
||||
>
|
||||
<!-- 自定义折叠状态下的会话项内容 -->
|
||||
<template #default="{ item }">
|
||||
<div class="conversation-item-content">
|
||||
<div v-if="isCollapsed" class="collapsed-item">
|
||||
<div
|
||||
class="avatar-circle"
|
||||
@click="handleChange(item)"
|
||||
@contextmenu="(e) => handleContextMenu(e, item)"
|
||||
>
|
||||
{{ item.sessionTitle?.charAt(0) || 'A' }}
|
||||
</div>
|
||||
<div v-if="item.unreadCount" class="unread-indicator">
|
||||
{{ item.unreadCount > 99 ? '99+' : item.unreadCount }}
|
||||
</div>
|
||||
<!-- 折叠状态下的更多操作按钮 -->
|
||||
<div
|
||||
class="collapsed-menu-trigger"
|
||||
@click.stop="(e) => handleCollapsedMenuClick(e, item)"
|
||||
@contextmenu.stop="(e) => handleContextMenu(e, item)"
|
||||
>
|
||||
<el-icon size="14">
|
||||
<MoreFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="expanded-item">
|
||||
<div class="conversation-info">
|
||||
<div class="conversation-title">
|
||||
{{ item.sessionTitle }}
|
||||
</div>
|
||||
<div v-if="item.sessionContent" class="conversation-preview">
|
||||
{{ item.sessionContent.substring(0, 30) }}{{ item.sessionContent.length > 30 ? '...' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Conversations>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-else
|
||||
class="h-full flex-center"
|
||||
:description="isCollapsed ? '' : '暂无对话记录'"
|
||||
>
|
||||
<template #description>
|
||||
<span v-if="!isCollapsed">暂无对话记录</span>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 基础样式
|
||||
.aside-container {
|
||||
height: 100%;
|
||||
border-right: 0.5px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
//background-color: var(--sidebar-background-color, #f9fafb);
|
||||
|
||||
// 展开状态 - 240px
|
||||
&:not(.aside-collapsed) {
|
||||
width: 240px;
|
||||
|
||||
.aside-wrapper {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠状态 - 100px
|
||||
&.aside-collapsed {
|
||||
display: none;
|
||||
|
||||
.aside-wrapper {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// 头部样式
|
||||
.aside-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
//background-color: var(--sidebar-header-bg, #ffffff);
|
||||
|
||||
.header-content-expanded {
|
||||
flex: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header-content-collapsed {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
transform: skewX(-2deg);
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-fill-color-light);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.aside-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//padding: 0 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.creat-chat-btn-wrapper {
|
||||
padding: 12px 8px 4px;
|
||||
|
||||
.creat-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
color: #0057ff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: rgb(0 87 255 / 6%);
|
||||
border: 1px solid rgb(0 102 255 / 15%);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
background-color: rgb(0 87 255 / 12%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.creat-chat-btn-collapsed {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.creat-chat-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
margin-left: 6px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.conversations-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.conversation-item-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.collapsed-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.avatar-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.unread-indicator {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.collapsed-menu-trigger {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 2;
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .collapsed-menu-trigger {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
.conversation-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.conversation-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.conversation-preview {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 样式穿透 - 重点修复溢出问题
|
||||
:deep() {
|
||||
.conversations-list {
|
||||
background-color: transparent !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.conversation-group-title {
|
||||
padding-left: 12px !important;
|
||||
background-color: transparent !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.title-text {
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
transition: all 0.3s ease;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
|
||||
&-inner {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
&-content {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
max-width: calc(100% - 32px) !important;
|
||||
overflow: hidden !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
// 确保操作按钮区域在展开状态下正常显示
|
||||
&-actions {
|
||||
flex-shrink: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.el-button {
|
||||
transition: all 0.2s ease;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
padding: 0 !important;
|
||||
margin-left: 4px !important;
|
||||
flex-shrink: 0 !important;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠状态样式
|
||||
.aside-collapsed {
|
||||
.conversation-group-title {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
justify-content: center !important;
|
||||
padding: 12px 8px !important;
|
||||
height: 64px !important;
|
||||
min-height: 64px !important;
|
||||
|
||||
&-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
// 折叠状态下隐藏默认操作按钮,使用自定义的
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
&-content {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 展开状态样式
|
||||
&:not(.aside-collapsed) {
|
||||
.conversation-item {
|
||||
&-actions {
|
||||
display: flex !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义对话框样式
|
||||
:deep(.collapsed-menu-dialog) {
|
||||
.el-message-box {
|
||||
width: 160px !important;
|
||||
padding: 12px !important;
|
||||
|
||||
&__header {
|
||||
padding: 0 0 8px 0 !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 14px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
&__headerbtn {
|
||||
top: 8px !important;
|
||||
right: 8px !important;
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.aside-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
width: 280px !important;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
background: #fff;
|
||||
|
||||
&.aside-collapsed {
|
||||
transform: translateX(-100%);
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
&:not(.aside-collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.aside-wrapper {
|
||||
width: 280px !important;
|
||||
|
||||
.aside-collapsed & {
|
||||
width: 100px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端遮罩层
|
||||
.aside-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -14,27 +14,6 @@ function openTutorial() {
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">文档</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon w-6 h-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 14l9-5-9-5-9 5 9 5z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 14l6.16-3.422A12.083 12.083 0 0118 13.5c0 2.579-3.582 4.5-6 4.5s-6-1.921-6-4.5c0-.432.075-.85.198-1.244L12 14z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -70,19 +49,4 @@ function openTutorial() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.ai-tutorial-btn-container {
|
||||
.ai-tutorial-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAnnouncementStore } from '@/stores';
|
||||
|
||||
const announcementStore = useAnnouncementStore();
|
||||
const { announcements } = storeToRefs(announcementStore);
|
||||
|
||||
// 计算未读公告数量(系统公告数量)
|
||||
const unreadCount = computed(() => {
|
||||
if (!Array.isArray(announcements.value))
|
||||
return 0;
|
||||
return announcements.value.filter(a => a.type === 'System').length;
|
||||
});
|
||||
|
||||
// 打开公告弹窗
|
||||
function openAnnouncement() {
|
||||
announcementStore.openDialog();
|
||||
}
|
||||
@@ -20,93 +10,127 @@ function openAnnouncement() {
|
||||
|
||||
<template>
|
||||
<div class="announcement-btn-container" data-tour="announcement-btn">
|
||||
<el-badge
|
||||
is-dot
|
||||
class="announcement-badge"
|
||||
<div
|
||||
class="announcement-btn"
|
||||
title="查看公告"
|
||||
@click="openAnnouncement"
|
||||
>
|
||||
<!-- :value="unreadCount" -->
|
||||
<!-- :hidden="unreadCount === 0" -->
|
||||
<!-- :max="99" -->
|
||||
<div
|
||||
class="announcement-btn"
|
||||
title="查看公告"
|
||||
@click="openAnnouncement"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">公告</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
</div>
|
||||
</el-badge>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">公告</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
.announcement-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.announcement-badge {
|
||||
:deep(.el-badge__content) {
|
||||
background-color: #f56c6c;
|
||||
border: none;
|
||||
}
|
||||
.announcement-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.announcement-btn:hover {
|
||||
color: #66b1ff;
|
||||
transform: translateY(-1px);
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
/* PC端文字样式 */
|
||||
.pc-text {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||
line-height: 1.2;
|
||||
padding: 2px 8px 2px 0;
|
||||
}
|
||||
|
||||
/* 红点样式 - 缩小到一半 */
|
||||
.pc-text::after,
|
||||
.mobile-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 6px; /* 缩小到6px */
|
||||
height: 6px; /* 缩小到6px */
|
||||
background-color: #f56c6c;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid #fff; /* 边框也相应缩小 */
|
||||
box-shadow: 0 1px 2px rgba(245, 108, 108, 0.3);
|
||||
animation: pulse 1.8s infinite;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* PC端红点位置 - 调整位置使红点正好与"告"字相交 */
|
||||
.pc-text::after {
|
||||
top: -4px; /* 向上移动,与文字相交 */
|
||||
right: -4px; /* 向右移动,与文字相交 */
|
||||
}
|
||||
|
||||
/* 为小红点添加微小的光晕效果 */
|
||||
.pc-text::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
width: 10px; /* 光晕也相应缩小 */
|
||||
height: 10px; /* 光晕也相应缩小 */
|
||||
background-color: rgba(245, 108, 108, 0.2);
|
||||
border-radius: 50%;
|
||||
animation: glow 2s infinite;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.announcement-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.announcement-btn-container {
|
||||
.announcement-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
/* 移动端图标样式 */
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
/* 移动端图标内的红点位置 */
|
||||
.mobile-icon::after {
|
||||
top: -2px; /* 位置微调 */
|
||||
right: -2px; /* 位置微调 */
|
||||
width: 5px; /* 移动端红点更小一点 */
|
||||
height: 5px; /* 移动端红点更小一点 */
|
||||
}
|
||||
|
||||
/* 呼吸动画效果 - 调整为更微妙的动画 */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
box-shadow: 0 1px 2px rgba(245, 108, 108, 0.2);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 4px rgba(245, 108, 108, 0.4);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
box-shadow: 0 1px 2px rgba(245, 108, 108, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
<!-- 头像 -->
|
||||
<script setup lang="ts">
|
||||
import { ChatLineRound } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Popover from '@/components/Popover/index.vue';
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||
import { useAnnouncementStore, useGuideTourStore, useUserStore } from '@/stores';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
import { getUserProfilePicture, isUserVip } from '@/utils/user';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const sessionStore = useSessionStore();
|
||||
const guideTourStore = useGuideTourStore();
|
||||
const announcementStore = useAnnouncementStore();
|
||||
const { startUserCenterTour } = useGuideTour();
|
||||
const { startHeaderTour } = useGuideTour();
|
||||
|
||||
/* 弹出面板 开始 */
|
||||
const popoverStyle = ref({
|
||||
@@ -29,41 +25,17 @@ const popoverRef = ref();
|
||||
|
||||
// 弹出面板内容
|
||||
const popoverList = ref([
|
||||
|
||||
{
|
||||
key: '5',
|
||||
title: '控制台',
|
||||
key: '1',
|
||||
title: '用户信息',
|
||||
icon: 'settings-4-fill',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
key: '7',
|
||||
title: '公告',
|
||||
icon: 'notification-fill',
|
||||
},
|
||||
{
|
||||
key: '8',
|
||||
title: '模型库',
|
||||
icon: 'apps-fill',
|
||||
},
|
||||
{
|
||||
key: '9',
|
||||
title: '文档',
|
||||
icon: 'book-fill',
|
||||
},
|
||||
|
||||
{
|
||||
key: '6',
|
||||
title: '新手引导',
|
||||
icon: 'dashboard-fill',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
divider: true,
|
||||
},
|
||||
// 待定
|
||||
// {
|
||||
// key: '6',
|
||||
// title: '新手引导',
|
||||
// icon: 'dashboard-fill',
|
||||
// },
|
||||
{
|
||||
key: '4',
|
||||
title: '退出登录',
|
||||
@@ -71,85 +43,20 @@ const popoverList = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const rechargeLogRef = ref();
|
||||
const activeNav = ref('user');
|
||||
|
||||
// ============ 邀请码分享功能 ============
|
||||
/** 从 URL 获取的邀请码 */
|
||||
const externalInviteCode = ref<string>('');
|
||||
|
||||
const navItems = [
|
||||
{ name: 'user', label: '用户信息', icon: 'User' },
|
||||
// { name: 'role', label: '角色管理', icon: 'Avatar' },
|
||||
// { name: 'permission', label: '权限管理', icon: 'Key' },
|
||||
// { name: 'userInfo', label: '用户信息', icon: 'User' },
|
||||
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
|
||||
|
||||
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' },
|
||||
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },
|
||||
{ name: 'premiumService', label: '尊享服务', icon: 'ColdDrink' },
|
||||
{ name: 'dailyTask', label: '每日任务(限时)', icon: 'Trophy' },
|
||||
{ name: 'cardFlip', label: '每周邀请(限时)', icon: 'Present' },
|
||||
// { name: 'usageStatistics2', label: '用量统计2', icon: 'Histogram' },
|
||||
{ name: 'activationCode', label: '激活码兑换', icon: 'MagicStick' },
|
||||
];
|
||||
function openDialog() {
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function handleConfirm(activeNav: string) {
|
||||
ElMessage.success('操作成功');
|
||||
}
|
||||
|
||||
// 导航切换
|
||||
function handleNavChange(nav: string) {
|
||||
activeNav.value = nav;
|
||||
// 同步更新 store 中的 tab 状态,防止下次通过 store 打开同一 tab 时因值未变而不触发 watch
|
||||
if (userStore.userCenterActiveTab !== nav) {
|
||||
userStore.userCenterActiveTab = nav;
|
||||
}
|
||||
}
|
||||
|
||||
// 联系售后
|
||||
function handleContactSupport() {
|
||||
rechargeLogRef.value?.contactCustomerService();
|
||||
}
|
||||
const { startHeaderTour } = useGuideTour();
|
||||
|
||||
// 开始引导教程
|
||||
function handleStartTutorial() {
|
||||
startHeaderTour();
|
||||
}
|
||||
|
||||
// 点击
|
||||
function handleClick(item: any) {
|
||||
switch (item.key) {
|
||||
case '1':
|
||||
ElMessage.warning('暂未开放');
|
||||
break;
|
||||
case '2':
|
||||
ElMessage.warning('暂未开放');
|
||||
break;
|
||||
case '5':
|
||||
openDialog();
|
||||
router.push('/console/user');
|
||||
break;
|
||||
case '6':
|
||||
handleStartTutorial();
|
||||
break;
|
||||
case '7':
|
||||
// 打开公告
|
||||
popoverRef.value?.hide?.();
|
||||
announcementStore.openDialog();
|
||||
break;
|
||||
case '8':
|
||||
// 打开模型库
|
||||
popoverRef.value?.hide?.();
|
||||
router.push('/model-library');
|
||||
break;
|
||||
case '9':
|
||||
// 打开文档
|
||||
popoverRef.value?.hide?.();
|
||||
window.open('https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde', '_blank');
|
||||
break;
|
||||
case '4':
|
||||
popoverRef.value?.hide?.();
|
||||
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
|
||||
@@ -173,10 +80,7 @@ function handleClick(item: any) {
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// ElMessage({
|
||||
// type: 'info',
|
||||
// message: '取消',
|
||||
// });
|
||||
// 取消退出,不执行任何操作
|
||||
});
|
||||
break;
|
||||
default:
|
||||
@@ -220,124 +124,15 @@ function openVipGuide() {
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 点击右上角关闭或“关闭”按钮,不执行任何操作
|
||||
// 点击右上角关闭或"关闭"按钮,不执行任何操作
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 监听对话框打开事件,切换到邀请码标签页 ============
|
||||
watch(dialogVisible, (newVal) => {
|
||||
if (newVal && externalInviteCode.value) {
|
||||
// 对话框打开后,切换标签页(已通过 :default-active 绑定,会自动响应)
|
||||
// console.log('[Avatar] watch: 对话框已打开,切换到 cardFlip 标签页');
|
||||
nextTick(() => {
|
||||
activeNav.value = 'cardFlip';
|
||||
// console.log('[Avatar] watch: 已设置 activeNav 为', activeNav.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 对话框关闭时,清除邀请码状态和 URL 参数
|
||||
if (!newVal && externalInviteCode.value) {
|
||||
// console.log('[Avatar] watch: 对话框关闭,清除邀请码状态');
|
||||
externalInviteCode.value = '';
|
||||
|
||||
// 清除 URL 中的 inviteCode 参数
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.has('inviteCode')) {
|
||||
url.searchParams.delete('inviteCode');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
// console.log('[Avatar] watch: 已清除 URL 中的 inviteCode 参数');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听 URL 参数,实现邀请码快捷分享 ============
|
||||
onMounted(() => {
|
||||
// 获取 URL 查询参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const inviteCode = urlParams.get('inviteCode');
|
||||
|
||||
if (inviteCode && inviteCode.trim()) {
|
||||
// console.log('[Avatar] onMounted: 检测到邀请码', inviteCode);
|
||||
|
||||
// 保存邀请码
|
||||
externalInviteCode.value = inviteCode.trim();
|
||||
|
||||
// 先设置标签页为 cardFlip
|
||||
activeNav.value = 'cardFlip';
|
||||
// console.log('[Avatar] onMounted: 设置 activeNav 为', activeNav.value);
|
||||
|
||||
// 延迟打开对话框,确保状态已更新
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// console.log('[Avatar] onMounted: 打开用户中心对话框');
|
||||
dialogVisible.value = true;
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// 注意:不立即清除 URL 参数,保留给登录后使用
|
||||
// URL 参数会在对话框关闭时清除
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听引导状态,自动打开用户中心并开始引导 ============
|
||||
watch(() => guideTourStore.shouldStartUserCenterTour, (shouldStart) => {
|
||||
if (shouldStart) {
|
||||
// 清除触发标记
|
||||
guideTourStore.clearUserCenterTourTrigger();
|
||||
|
||||
// 注册导航切换回调
|
||||
guideTourStore.setUserCenterNavChangeCallback((nav: string) => {
|
||||
activeNav.value = nav;
|
||||
});
|
||||
|
||||
// 注册关闭弹窗回调
|
||||
guideTourStore.setUserCenterCloseCallback(() => {
|
||||
dialogVisible.value = false;
|
||||
});
|
||||
|
||||
// 打开用户中心弹窗
|
||||
nextTick(() => {
|
||||
dialogVisible.value = true;
|
||||
|
||||
// 等待弹窗打开后开始引导
|
||||
setTimeout(() => {
|
||||
startUserCenterTour();
|
||||
}, 600);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听 Store 状态,控制用户中心弹窗 (新增) ============
|
||||
watch(() => userStore.isUserCenterVisible, (val) => {
|
||||
dialogVisible.value = val;
|
||||
if (val && userStore.userCenterActiveTab) {
|
||||
activeNav.value = userStore.userCenterActiveTab;
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => userStore.userCenterActiveTab, (val) => {
|
||||
if (val) {
|
||||
activeNav.value = val;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听本地 dialogVisible 变化,同步回 Store(可选,为了保持一致性)
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
userStore.closeUserCenter();
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 暴露方法供外部调用 ============
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2 ">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 用户信息区域 -->
|
||||
<div class="user-info-display cursor-pointer flex flex-col text-right mr-2 leading-tight" @click="openDialog">
|
||||
<div class="user-info-display cursor-pointer flex flex-col text-right mr-2 leading-tight">
|
||||
<div class="text-sm font-semibold text-gray-800">
|
||||
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
|
||||
</div>
|
||||
@@ -354,6 +149,7 @@ defineExpose({
|
||||
<span
|
||||
v-else
|
||||
class="inline-block px-2 py-0.5 text-xs text-gray-600 bg-gray-100 rounded-full cursor-pointer hover:bg-yellow-50 hover:text-yellow-700 transition"
|
||||
@click="openVipGuide"
|
||||
>
|
||||
普通用户
|
||||
</span>
|
||||
@@ -361,7 +157,7 @@ defineExpose({
|
||||
</div>
|
||||
|
||||
<!-- 头像区域 -->
|
||||
<div class="avatar-container" data-tour="user-avatar">
|
||||
<div class="avatar-container">
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
placement="bottom-end"
|
||||
@@ -393,6 +189,7 @@ defineExpose({
|
||||
<span
|
||||
v-else
|
||||
class="inline-block px-2 py-0.5 text-xs text-gray-600 bg-gray-100 rounded-full cursor-pointer hover:bg-yellow-50 hover:text-yellow-700 transition"
|
||||
@click="openVipGuide"
|
||||
>
|
||||
普通用户
|
||||
</span>
|
||||
@@ -403,7 +200,6 @@ defineExpose({
|
||||
|
||||
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
|
||||
<div
|
||||
v-if="!item.divider"
|
||||
class="popover-content-box-item flex items-center h-full gap-8px p-8px pl-10px pr-12px rounded-lg hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
@@ -412,78 +208,15 @@ defineExpose({
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.divider" class="divder h-1px bg-gray-200 my-4px" />
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
<nav-dialog
|
||||
v-model="dialogVisible"
|
||||
title="控制台"
|
||||
:nav-items="navItems"
|
||||
:default-active="activeNav"
|
||||
@confirm="handleConfirm"
|
||||
@nav-change="handleNavChange"
|
||||
>
|
||||
<template #extra-actions>
|
||||
<el-tooltip v-if="isUserVip() && activeNav === 'rechargeLog'" content="联系售后" placement="bottom">
|
||||
<el-button circle plain size="small" @click="handleContactSupport">
|
||||
<el-icon color="#07c160">
|
||||
<ChatLineRound />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<!-- 用户管理内容 -->
|
||||
<template #user>
|
||||
<user-management />
|
||||
</template>
|
||||
<!-- 用量统计 -->
|
||||
<template #usageStatistics>
|
||||
<usage-statistics />
|
||||
</template>
|
||||
<!-- 尊享服务 -->
|
||||
<template #premiumService>
|
||||
<premium-service />
|
||||
</template>
|
||||
<!-- 用量统计 -->
|
||||
<!-- <template #usageStatistics2> -->
|
||||
<!-- <usage-statistics2 /> -->
|
||||
<!-- </template> -->
|
||||
|
||||
<!-- 角色管理内容 -->
|
||||
<template #role>
|
||||
<!-- < /> -->
|
||||
</template>
|
||||
|
||||
<!-- 权限管理内容 -->
|
||||
<template #permission>
|
||||
<!-- <permission-management /> -->
|
||||
</template>
|
||||
|
||||
<template #apiKey>
|
||||
<APIKeyManagement />
|
||||
</template>
|
||||
<template #activationCode>
|
||||
<activation-code />
|
||||
</template>
|
||||
<template #dailyTask>
|
||||
<daily-task />
|
||||
</template>
|
||||
<template #cardFlip>
|
||||
<card-flip-activity :external-invite-code="externalInviteCode" />
|
||||
</template>
|
||||
<template #rechargeLog>
|
||||
<recharge-log ref="rechargeLogRef" />
|
||||
</template>
|
||||
</nav-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popover-content {
|
||||
.popover-content{
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 检查是否在聊天页面
|
||||
const isOnChatPage = computed(() => {
|
||||
return route.path.startsWith('/chat');
|
||||
});
|
||||
|
||||
function goToChat() {
|
||||
router.push('/chat/conversation');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!isOnChatPage" class="start-chat-btn-container" data-tour="start-chat-btn">
|
||||
<div
|
||||
class="start-chat-btn"
|
||||
title="开始聊天"
|
||||
@click="goToChat"
|
||||
>
|
||||
<el-icon class="chat-icon">
|
||||
<i-ep-chat-dot-round />
|
||||
</el-icon>
|
||||
<span class="btn-text">开始聊天</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.start-chat-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 12px;
|
||||
|
||||
.start-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.chat-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.start-chat-btn-container {
|
||||
margin-right: 8px;
|
||||
|
||||
.start-chat-btn {
|
||||
padding: 8px;
|
||||
|
||||
.btn-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core';
|
||||
|
||||
// 使用 VueUse 的 useColorMode
|
||||
const mode = useColorMode({
|
||||
attribute: 'class',
|
||||
modes: {
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
},
|
||||
});
|
||||
|
||||
// 切换主题
|
||||
function toggleTheme() {
|
||||
mode.value = mode.value === 'dark' ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
// 主题图标
|
||||
const themeIcon = computed(() => {
|
||||
return mode.value === 'dark' ? 'Sunny' : 'Moon';
|
||||
});
|
||||
|
||||
// 主题标题
|
||||
const themeTitle = computed(() => {
|
||||
return mode.value === 'dark' ? '切换到浅色模式' : '切换到深色模式';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-btn-container" data-tour="theme-btn">
|
||||
<div
|
||||
class="theme-btn"
|
||||
:title="themeTitle"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<!-- PC端显示文字 + 图标 -->
|
||||
<el-icon class="theme-icon">
|
||||
<component :is="`i-ep-${themeIcon}`" />
|
||||
</el-icon>
|
||||
<span class="pc-text">主题</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.theme-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.theme-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
font-size: 18px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
&:hover .theme-icon {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
// PC端显示文字
|
||||
.pc-text {
|
||||
display: inline;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.theme-btn-container {
|
||||
.theme-btn {
|
||||
padding: 8px;
|
||||
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,117 +1,624 @@
|
||||
<!-- Header 头部 -->
|
||||
<script setup lang="ts">
|
||||
import { onKeyStroke } from '@vueuse/core';
|
||||
import { SIDE_BAR_WIDTH } from '@/config/index';
|
||||
import { useDesignStore, useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import logo from '@/assets/images/logo.png';
|
||||
import ConsoleBtn from '@/layouts/components0/Header/components/ConsoleBtn.vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
import AiTutorialBtn from './components/AiTutorialBtn.vue';
|
||||
import AnnouncementBtn from './components/AnnouncementBtn.vue';
|
||||
import Avatar from './components/Avatar.vue';
|
||||
import BuyBtn from './components/BuyBtn.vue';
|
||||
import Collapse from './components/Collapse.vue';
|
||||
import ConsoleBtn from './components/ConsoleBtn.vue';
|
||||
import CreateChat from './components/CreateChat.vue';
|
||||
import LoginBtn from './components/LoginBtn.vue';
|
||||
import ModelLibraryBtn from './components/ModelLibraryBtn.vue';
|
||||
import TitleEditing from './components/TitleEditing.vue';
|
||||
import ThemeBtn from './components/ThemeBtn.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const avatarRef = ref();
|
||||
// 移动端菜单抽屉状态
|
||||
const mobileMenuVisible = ref(false);
|
||||
|
||||
const currentSession = computed(() => sessionStore.currentSession);
|
||||
|
||||
onMounted(() => {
|
||||
// 全局设置侧边栏默认宽度 (这个是不变的,一开始就设置)
|
||||
document.documentElement.style.setProperty(`--sidebar-default-width`, `${SIDE_BAR_WIDTH}px`);
|
||||
if (designStore.isCollapse) {
|
||||
document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``);
|
||||
}
|
||||
else {
|
||||
document.documentElement.style.setProperty(
|
||||
`--sidebar-left-container-default-width`,
|
||||
`${SIDE_BAR_WIDTH}px`,
|
||||
);
|
||||
}
|
||||
// 当前激活的菜单项
|
||||
const activeIndex = computed(() => {
|
||||
if (route.path.startsWith('/console'))
|
||||
return 'console';
|
||||
if (route.path.startsWith('/model-library'))
|
||||
return 'model-library';
|
||||
if (route.path.includes('/chat/'))
|
||||
return 'chat';
|
||||
return '';
|
||||
});
|
||||
|
||||
// 定义 Ctrl+K 的处理函数
|
||||
function handleCtrlK(event: KeyboardEvent) {
|
||||
event.preventDefault(); // 防止默认行为
|
||||
sessionStore.createSessionBtn();
|
||||
// 导航处理
|
||||
function handleSelect(key: string) {
|
||||
if (key && key !== 'no-route') {
|
||||
router.push(key);
|
||||
mobileMenuVisible.value = false; // 移动端导航后关闭菜单
|
||||
}
|
||||
}
|
||||
|
||||
// 设置全局的键盘按键监听
|
||||
onKeyStroke(event => event.ctrlKey && event.key.toLowerCase() === 'k', handleCtrlK, {
|
||||
passive: false,
|
||||
});
|
||||
// 修改 AI 聊天菜单的点击事件
|
||||
function handleAIClick(e: MouseEvent) {
|
||||
e.stopPropagation(); // 阻止事件冒泡
|
||||
router.push('/chat/conversation');
|
||||
mobileMenuVisible.value = false;
|
||||
}
|
||||
|
||||
// 打开控制台
|
||||
function handleOpenConsole() {
|
||||
avatarRef.value?.openDialog?.();
|
||||
// 修改控制台菜单的点击事件
|
||||
function handleConsoleClick(e: MouseEvent) {
|
||||
e.stopPropagation(); // 阻止事件冒泡
|
||||
router.push('/console/user');
|
||||
mobileMenuVisible.value = false;
|
||||
}
|
||||
|
||||
// 切换移动端菜单
|
||||
function toggleMobileMenu() {
|
||||
mobileMenuVisible.value = !mobileMenuVisible.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<div class="header-box relative z-10 top-0 left-0 right-0">
|
||||
<div class="absolute left-0 right-0 top-0 bottom-0 flex items-center flex-row">
|
||||
<div
|
||||
class="overflow-hidden flex h-full items-center flex-row flex-1 w-fit flex-shrink-0 min-w-0"
|
||||
>
|
||||
<div class="w-full flex items-center flex-row">
|
||||
<!-- 左边 -->
|
||||
<div
|
||||
v-if="designStore.isCollapse"
|
||||
class="left-box flex h-full items-center pl-20px gap-12px flex-shrink-0 flex-row"
|
||||
>
|
||||
<Collapse />
|
||||
<CreateChat />
|
||||
<div v-if="currentSession" class="w-0.5px h-30px bg-[rgba(217,217,217)]" />
|
||||
</div>
|
||||
|
||||
<!-- 中间 -->
|
||||
<div class="middle-box flex-1 min-w-0 ml-12px">
|
||||
<TitleEditing />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右边 -->
|
||||
<div class="right-box flex h-full items-center pr-20px flex-shrink-0 mr-auto flex-row">
|
||||
<AnnouncementBtn />
|
||||
<ModelLibraryBtn />
|
||||
<AiTutorialBtn />
|
||||
<ConsoleBtn @open-console="handleOpenConsole" />
|
||||
<BuyBtn v-show="userStore.userInfo" />
|
||||
<Avatar v-show="userStore.userInfo" ref="avatarRef" />
|
||||
<LoginBtn v-show="!userStore.userInfo" />
|
||||
<!-- 桌面端菜单 -->
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="header-menu desktop-menu"
|
||||
mode="horizontal"
|
||||
:ellipsis="false"
|
||||
:router="false"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<!-- 左侧品牌区域 -->
|
||||
<div class="menu-left">
|
||||
<div class="brand-container" @click="router.push('/')">
|
||||
<el-image :src="logo" alt="logo" fit="contain" class="logo-img" />
|
||||
<span class="brand-text">意心AI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧功能区域 -->
|
||||
<div class="menu-right">
|
||||
<!-- AI聊天菜单 -->
|
||||
<el-sub-menu index="chat" class="chat-submenu" popper-class="custom-popover">
|
||||
<template #title>
|
||||
<span class="menu-title" @click="handleAIClick">AI应用</span>
|
||||
</template>
|
||||
<el-menu-item index="/chat/conversation">
|
||||
AI对话
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/chat/image">
|
||||
AI图片
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/chat/video">
|
||||
AI视频
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/chat/agent">
|
||||
AI智能体
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 公告按钮 -->
|
||||
<el-menu-item class="custom-menu-item" index="no-route">
|
||||
<AnnouncementBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 模型库 -->
|
||||
<el-menu-item index="/model-library" class="custom-menu-item">
|
||||
<ModelLibraryBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- AI教程 -->
|
||||
<el-menu-item class="custom-menu-item" index="no-route">
|
||||
<AiTutorialBtn />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 控制台菜单 -->
|
||||
<el-sub-menu index="console" class="console-submenu" popper-class="custom-popover">
|
||||
<template #title>
|
||||
<ConsoleBtn @click="handleConsoleClick" />
|
||||
</template>
|
||||
<el-menu-item index="/console/user">
|
||||
用户信息
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/apikey">
|
||||
API密钥
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/recharge-log">
|
||||
充值记录
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/usage">
|
||||
用量统计
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/premium">
|
||||
尊享服务
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/daily-task">
|
||||
每日任务
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/invite">
|
||||
每周邀请
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/activation">
|
||||
激活码兑换
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 购买按钮 -->
|
||||
<el-menu-item v-if="userStore.userInfo" class="custom-menu-item" index="no-route">
|
||||
<BuyBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 主题切换(暂不显示) -->
|
||||
<el-menu-item v-if="false" class="custom-menu-item" index="no-route">
|
||||
<ThemeBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<div v-if="userStore.userInfo" class="avatar-container">
|
||||
<Avatar />
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<el-menu-item v-if="!userStore.userInfo" class="login-menu-item" index="no-route">
|
||||
<LoginBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
</div>
|
||||
</el-menu>
|
||||
|
||||
<!-- 移动端头部 -->
|
||||
<div class="mobile-header">
|
||||
<div class="mobile-brand" @click="router.push('/')">
|
||||
<el-image :src="logo" alt="logo" fit="contain" class="mobile-logo" />
|
||||
<span class="mobile-brand-text">意心AI</span>
|
||||
</div>
|
||||
|
||||
<div class="mobile-actions">
|
||||
<!-- 用户头像或登录按钮 -->
|
||||
<div v-if="userStore.userInfo" class="mobile-avatar">
|
||||
<Avatar />
|
||||
</div>
|
||||
<LoginBtn v-else :is-menu-item="false" />
|
||||
|
||||
<!-- 汉堡菜单按钮 -->
|
||||
<el-button class="menu-toggle" text @click="toggleMobileMenu">
|
||||
<el-icon :size="24">
|
||||
<component :is="mobileMenuVisible ? 'Close' : 'Menu'" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端抽屉菜单 -->
|
||||
<el-drawer
|
||||
v-model="mobileMenuVisible"
|
||||
direction="rtl"
|
||||
:size="280"
|
||||
:show-close="false"
|
||||
class="mobile-drawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="drawer-header">
|
||||
<span class="drawer-title">菜单</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="mobile-menu"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<!-- AI应用 -->
|
||||
<el-sub-menu index="chat">
|
||||
<template #title>
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
<span>AI应用</span>
|
||||
</template>
|
||||
<el-menu-item index="/chat/conversation">AI对话</el-menu-item>
|
||||
<el-menu-item index="/chat/image">AI图片</el-menu-item>
|
||||
<el-menu-item index="/chat/video">AI视频</el-menu-item>
|
||||
<el-menu-item index="/chat/agent">AI智能体</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 模型库 -->
|
||||
<el-menu-item index="/model-library">
|
||||
<el-icon><Box /></el-icon>
|
||||
<span>模型库</span>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 控制台 -->
|
||||
<el-sub-menu index="console">
|
||||
<template #title>
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>控制台</span>
|
||||
</template>
|
||||
<el-menu-item index="/console/user">用户信息</el-menu-item>
|
||||
<el-menu-item index="/console/apikey">API密钥</el-menu-item>
|
||||
<el-menu-item index="/console/recharge-log">充值记录</el-menu-item>
|
||||
<el-menu-item index="/console/usage">用量统计</el-menu-item>
|
||||
<el-menu-item index="/console/premium">尊享服务</el-menu-item>
|
||||
<el-menu-item index="/console/daily-task">每日任务</el-menu-item>
|
||||
<el-menu-item index="/console/invite">每周邀请</el-menu-item>
|
||||
<el-menu-item index="/console/activation">激活码兑换</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 其他功能 -->
|
||||
<div class="mobile-menu-actions">
|
||||
<div class="action-item">
|
||||
<AnnouncementBtn :is-menu-item="false" />
|
||||
</div>
|
||||
<div class="action-item">
|
||||
<AiTutorialBtn />
|
||||
</div>
|
||||
<div v-if="userStore.userInfo" class="action-item">
|
||||
<BuyBtn :is-menu-item="false" />
|
||||
</div>
|
||||
</div>
|
||||
</el-menu>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
--menu-hover-bg: var(--color-white);
|
||||
--menu-active-color: var(--el-color-primary);
|
||||
--menu-transition: all 0.2s ease;
|
||||
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
.header-box {
|
||||
width: 100%;
|
||||
width: calc(
|
||||
100% - var(--sidebar-left-container-default-width, 0px) - var(
|
||||
--sidebar-right-container-default-width,
|
||||
0px
|
||||
)
|
||||
);
|
||||
height: var(--header-container-default-heigth);
|
||||
margin: 0 var(--sidebar-right-container-default-width, 0) 0
|
||||
var(--sidebar-left-container-default-width, 0);
|
||||
height: var(--header-container-default-height, 64px);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 移动端头部(默认隐藏)
|
||||
.mobile-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-menu {
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
border-bottom: none !important;
|
||||
//background: var(--color-white);
|
||||
|
||||
}
|
||||
|
||||
// 左侧品牌区域
|
||||
.menu-left {
|
||||
flex-shrink: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.brand-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: background-color var(--menu-transition);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
transition: transform var(--menu-transition);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--brand-color, #000000);
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
// 右侧功能区域
|
||||
.menu-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin-right: 16px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
// 公共菜单项样式
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: 100% !important;
|
||||
border-bottom: none !important;
|
||||
padding: 0 4px !important;
|
||||
color: inherit !important;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
color: var(--menu-active-color) !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: transparent !important;
|
||||
color: var(--menu-active-color) !important;
|
||||
|
||||
.menu-title {
|
||||
color: var(--menu-active-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天和控制台子菜单
|
||||
.chat-submenu,
|
||||
.console-submenu {
|
||||
:deep(.el-sub-menu__title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
// 自定义按钮菜单项
|
||||
.custom-menu-item,
|
||||
.login-menu-item {
|
||||
:deep(.el-menu-item-content) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar 容器
|
||||
.avatar-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
// 移动端头部样式
|
||||
.mobile-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-brand-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--brand-color, #000000);
|
||||
}
|
||||
|
||||
.mobile-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
padding: 8px;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端抽屉样式
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
border: none;
|
||||
|
||||
:deep(.el-sub-menu__title),
|
||||
:deep(.el-menu-item) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
padding: 0 20px !important;
|
||||
margin: 4px 0;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
margin-right: 12px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu-actions {
|
||||
margin-top: 20px;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid var(--el-border-color-light);
|
||||
|
||||
.action-item {
|
||||
padding: 8px 20px;
|
||||
margin: 4px 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1280px) {
|
||||
.brand-text {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.menu-right {
|
||||
margin-right: 12px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.brand-container {
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
// 隐藏桌面端菜单
|
||||
.desktop-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// 显示移动端头部
|
||||
.mobile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.mobile-header {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.mobile-brand-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mobile-logo {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.mobile-actions {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// 自定义弹出框样式
|
||||
.custom-popover {
|
||||
.el-menu {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 6px 0;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.el-menu-item {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0 20px;
|
||||
margin: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
725
Yi.Ai.Vue3/src/layouts/components0/Aside/index.vue
Normal file
725
Yi.Ai.Vue3/src/layouts/components0/Aside/index.vue
Normal file
@@ -0,0 +1,725 @@
|
||||
<script setup lang="ts">
|
||||
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations';
|
||||
import type { ChatSessionVo } from '@/api/session/types';
|
||||
import { ChatLineSquare, Expand, Fold, MoreFilled, Plus } from '@element-plus/icons-vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { get_session } from '@/api';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const sessionId = computed(() => route.params?.id);
|
||||
const conversationsList = computed(() => sessionStore.sessionList);
|
||||
const loadMoreLoading = computed(() => sessionStore.isLoadingMore);
|
||||
const active = ref<string | undefined>();
|
||||
const isCollapsed = computed(() => designStore.isCollapseConversationList);
|
||||
|
||||
onMounted(async () => {
|
||||
await sessionStore.requestSessionList();
|
||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sessionStore.currentSession,
|
||||
(newValue) => {
|
||||
active.value = newValue ? `${newValue.id}` : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
// 创建会话
|
||||
function handleCreatChat() {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function handleChange(item: ConversationItem<ChatSessionVo>) {
|
||||
sessionStore.setCurrentSession(item);
|
||||
router.replace({
|
||||
name: 'chatConversationWithId',
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 处理组件触发的加载更多事件
|
||||
async function handleLoadMore() {
|
||||
if (!sessionStore.hasMore)
|
||||
return;
|
||||
await sessionStore.loadMoreSessions();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo>) {
|
||||
switch (command) {
|
||||
case 'delete':
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
break;
|
||||
case 'rename':
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: item.sessionTitle,
|
||||
autofocus: false,
|
||||
inputValidator: (value) => {
|
||||
return !!value;
|
||||
},
|
||||
}).then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: item.id!,
|
||||
sessionTitle: value,
|
||||
sessionContent: item.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
if (sessionStore.currentSession?.id === item.id) {
|
||||
sessionStore.setCurrentSession({
|
||||
...item,
|
||||
sessionTitle: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠/展开侧边栏
|
||||
function toggleSidebar() {
|
||||
designStore.setIsCollapseConversationList(!designStore.isCollapseConversationList);
|
||||
}
|
||||
|
||||
// 点击logo创建新会话(仅在折叠状态)
|
||||
function handleLogoClick() {
|
||||
if (isCollapsed.value) {
|
||||
handleCreatChat();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理右键菜单
|
||||
function handleContextMenu(event: MouseEvent, item: ConversationItem<ChatSessionVo>) {
|
||||
event.preventDefault();
|
||||
// 在折叠状态下触发菜单
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="aside-container"
|
||||
:class="{ 'aside-collapsed': isCollapsed }"
|
||||
>
|
||||
<div class="aside-wrapper">
|
||||
<!-- 头部 -->
|
||||
<div class="aside-header">
|
||||
<!-- 展开状态显示logo和标题 -->
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
class="header-content-expanded flex items-center gap-8px hover:cursor-pointer"
|
||||
@click="handleCreatChat"
|
||||
>
|
||||
<span class="logo-text max-w-150px text-overflow">会话</span>
|
||||
</div>
|
||||
|
||||
<!-- 折叠状态只显示logo -->
|
||||
<div
|
||||
v-else
|
||||
class="header-content-collapsed flex items-center justify-center hover:cursor-pointer"
|
||||
@click="handleLogoClick"
|
||||
>
|
||||
<el-icon size="20">
|
||||
<ChatLineSquare />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 折叠按钮 -->
|
||||
<el-tooltip
|
||||
:content="isCollapsed ? '展开侧边栏' : '折叠侧边栏'"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button
|
||||
class="collapse-btn"
|
||||
type="text"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<el-icon v-if="isCollapsed">
|
||||
<Expand />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Fold />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="aside-body">
|
||||
<!-- 创建会话按钮 -->
|
||||
<div class="creat-chat-btn-wrapper">
|
||||
<div
|
||||
class="creat-chat-btn"
|
||||
:class="{ 'creat-chat-btn-collapsed': isCollapsed }"
|
||||
@click="handleCreatChat"
|
||||
>
|
||||
<el-icon class="add-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span v-if="!isCollapsed" class="creat-chat-text">
|
||||
新对话
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
<div class="aside-content">
|
||||
<div v-if="conversationsList.length > 0" class="conversations-wrap">
|
||||
<Conversations
|
||||
v-model:active="active"
|
||||
:items="conversationsList"
|
||||
:label-max-width="200"
|
||||
:show-tooltip="!isCollapsed"
|
||||
:tooltip-offset="60"
|
||||
show-built-in-menu
|
||||
groupable
|
||||
row-key="id"
|
||||
label-key="sessionTitle"
|
||||
:tooltip-placement="isCollapsed ? 'right-start' : 'right'"
|
||||
:load-more="handleLoadMore"
|
||||
:load-more-loading="loadMoreLoading"
|
||||
:items-style="{
|
||||
marginLeft: '8px',
|
||||
marginRight: '8px',
|
||||
userSelect: 'none',
|
||||
borderRadius: isCollapsed ? '12px' : '10px',
|
||||
padding: isCollapsed ? '12px 8px' : '8px 12px',
|
||||
justifyContent: isCollapsed ? 'center' : 'space-between',
|
||||
width: isCollapsed ? '64px' : 'auto',
|
||||
height: isCollapsed ? '64px' : 'auto',
|
||||
minHeight: '48px',
|
||||
flexDirection: isCollapsed ? 'column' : 'row',
|
||||
position: 'relative',
|
||||
}"
|
||||
:items-active-style="{
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
}"
|
||||
:items-hover-style="{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||
}"
|
||||
@menu-command="handleMenuCommand"
|
||||
@change="handleChange"
|
||||
@contextmenu="handleContextMenu"
|
||||
>
|
||||
<!-- 自定义折叠状态下的会话项内容 -->
|
||||
<template #default="{ item }">
|
||||
<div class="conversation-item-content">
|
||||
<div v-if="isCollapsed" class="collapsed-item">
|
||||
<div
|
||||
class="avatar-circle"
|
||||
@contextmenu="(e) => handleContextMenu(e, item)"
|
||||
>
|
||||
{{ item.sessionTitle?.charAt(0) || 'A' }}
|
||||
</div>
|
||||
<div v-if="item.unreadCount" class="unread-indicator">
|
||||
{{ item.unreadCount }}
|
||||
</div>
|
||||
<!-- 折叠状态下的更多操作按钮 -->
|
||||
<div
|
||||
class="collapsed-menu-trigger"
|
||||
@click.stop="handleMenuCommand('rename', item)"
|
||||
@contextmenu.stop="(e) => handleContextMenu(e, item)"
|
||||
>
|
||||
<el-icon size="14">
|
||||
<MoreFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="expanded-item">
|
||||
<div class="conversation-info">
|
||||
<div class="conversation-title">
|
||||
{{ item.sessionTitle }}
|
||||
</div>
|
||||
<div v-if="item.sessionContent" class="conversation-preview">
|
||||
{{ item.sessionContent.substring(0, 30) }}{{ item.sessionContent.length > 30 ? '...' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 展开状态下的更多操作按钮(Conversations组件自带) -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Conversations>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-else
|
||||
class="h-full flex-center"
|
||||
:description="isCollapsed ? '' : '暂无对话记录'"
|
||||
>
|
||||
<template #description>
|
||||
<span v-if="!isCollapsed">暂无对话记录</span>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 基础样式
|
||||
.aside-container {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
border-right: 0.5px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--sidebar-background-color, #f9fafb);
|
||||
|
||||
&.aside-collapsed {
|
||||
width: 100px;
|
||||
|
||||
.aside-wrapper {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.conversations-wrap {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 240px;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// 头部样式
|
||||
.aside-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
background-color: var(--sidebar-header-bg, #ffffff);
|
||||
|
||||
.header-content-expanded {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-content-collapsed {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
transform: skewX(-2deg);
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-fill-color-light);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.aside-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//padding: 0 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.creat-chat-btn-wrapper {
|
||||
padding: 12px 8px 4px;
|
||||
|
||||
.creat-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
color: #0057ff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: rgb(0 87 255 / 6%);
|
||||
border: 1px solid rgb(0 102 255 / 15%);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(0 87 255 / 12%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.creat-chat-btn-collapsed {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.creat-chat-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
margin-left: 6px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.conversations-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.conversation-item-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.collapsed-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.avatar-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.unread-indicator {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.collapsed-menu-trigger {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 2;
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .collapsed-menu-trigger {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.conversation-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
.conversation-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.conversation-preview {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 样式穿透 - 重点优化操作按钮区域
|
||||
:deep() {
|
||||
.conversations-list {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.conversation-group-title {
|
||||
padding-left: 12px !important;
|
||||
background-color: transparent !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.title-text {
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// 确保操作按钮区域在折叠状态下可见
|
||||
.conversation-item-actions {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.el-button {
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠状态样式
|
||||
.aside-collapsed {
|
||||
.conversation-group-title {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
justify-content: center !important;
|
||||
padding: 12px 8px !important;
|
||||
height: 64px !important;
|
||||
min-height: 64px !important;
|
||||
|
||||
&-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
// 隐藏默认的操作按钮,使用自定义的
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.aside-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
width: 280px !important;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
&.aside-collapsed {
|
||||
transform: translateX(-100%);
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
&:not(.aside-collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.aside-wrapper {
|
||||
width: 280px !important;
|
||||
|
||||
.aside-collapsed & {
|
||||
width: 100px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端遮罩层
|
||||
.aside-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
<!-- DesignConfig -->
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>配置页面</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
// 打开AI使用教程(跳转到外部链接)
|
||||
function openTutorial() {
|
||||
window.open('https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde', '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ai-tutorial-btn-container" data-tour="ai-tutorial-link">
|
||||
<div
|
||||
class="ai-tutorial-btn"
|
||||
title="点击跳转YiXinAI玩法指南专栏"
|
||||
@click="openTutorial"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">文档</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon w-6 h-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 14l9-5-9-5-9 5 9 5z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 14l6.16-3.422A12.083 12.083 0 0118 13.5c0 2.579-3.582 4.5-6 4.5s-6-1.921-6-4.5c0-.432.075-.85.198-1.244L12 14z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-tutorial-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ai-tutorial-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #E6A23C;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #F1B44C;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.ai-tutorial-btn-container {
|
||||
.ai-tutorial-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAnnouncementStore } from '@/stores';
|
||||
|
||||
const announcementStore = useAnnouncementStore();
|
||||
const { announcements } = storeToRefs(announcementStore);
|
||||
|
||||
// 计算未读公告数量(系统公告数量)
|
||||
const unreadCount = computed(() => {
|
||||
if (!Array.isArray(announcements.value))
|
||||
return 0;
|
||||
return announcements.value.filter(a => a.type === 'System').length;
|
||||
});
|
||||
|
||||
// 打开公告弹窗
|
||||
function openAnnouncement() {
|
||||
announcementStore.openDialog();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="announcement-btn-container" data-tour="announcement-btn">
|
||||
<el-badge
|
||||
is-dot
|
||||
class="announcement-badge"
|
||||
>
|
||||
<!-- :value="unreadCount" -->
|
||||
<!-- :hidden="unreadCount === 0" -->
|
||||
<!-- :max="99" -->
|
||||
<div
|
||||
class="announcement-btn"
|
||||
title="查看公告"
|
||||
@click="openAnnouncement"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">公告</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
</div>
|
||||
</el-badge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.announcement-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.announcement-badge {
|
||||
:deep(.el-badge__content) {
|
||||
background-color: #f56c6c;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.announcement-btn-container {
|
||||
.announcement-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
499
Yi.Ai.Vue3/src/layouts/components0/Header/components/Avatar.vue
Normal file
499
Yi.Ai.Vue3/src/layouts/components0/Header/components/Avatar.vue
Normal file
@@ -0,0 +1,499 @@
|
||||
<!-- 头像 -->
|
||||
<script setup lang="ts">
|
||||
import { ChatLineRound } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Popover from '@/components/Popover/index.vue';
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||
import { useAnnouncementStore, useGuideTourStore, useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
import { getUserProfilePicture, isUserVip } from '@/utils/user';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const sessionStore = useSessionStore();
|
||||
const guideTourStore = useGuideTourStore();
|
||||
const announcementStore = useAnnouncementStore();
|
||||
const { startUserCenterTour } = useGuideTour();
|
||||
|
||||
/* 弹出面板 开始 */
|
||||
const popoverStyle = ref({
|
||||
width: '200px',
|
||||
padding: '4px',
|
||||
height: 'fit-content',
|
||||
});
|
||||
const popoverRef = ref();
|
||||
|
||||
// 弹出面板内容
|
||||
const popoverList = ref([
|
||||
|
||||
{
|
||||
key: '5',
|
||||
title: '控制台',
|
||||
icon: 'settings-4-fill',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
key: '7',
|
||||
title: '公告',
|
||||
icon: 'notification-fill',
|
||||
},
|
||||
{
|
||||
key: '8',
|
||||
title: '模型库',
|
||||
icon: 'apps-fill',
|
||||
},
|
||||
{
|
||||
key: '9',
|
||||
title: '文档',
|
||||
icon: 'book-fill',
|
||||
},
|
||||
|
||||
{
|
||||
key: '6',
|
||||
title: '新手引导',
|
||||
icon: 'dashboard-fill',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
title: '退出登录',
|
||||
icon: 'logout-box-r-line',
|
||||
},
|
||||
]);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const rechargeLogRef = ref();
|
||||
const activeNav = ref('user');
|
||||
|
||||
// ============ 邀请码分享功能 ============
|
||||
/** 从 URL 获取的邀请码 */
|
||||
const externalInviteCode = ref<string>('');
|
||||
|
||||
const navItems = [
|
||||
{ name: 'user', label: '用户信息', icon: 'User' },
|
||||
// { name: 'role', label: '角色管理', icon: 'Avatar' },
|
||||
// { name: 'permission', label: '权限管理', icon: 'Key' },
|
||||
// { name: 'userInfo', label: '用户信息', icon: 'User' },
|
||||
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
|
||||
|
||||
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' },
|
||||
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },
|
||||
{ name: 'premiumService', label: '尊享服务', icon: 'ColdDrink' },
|
||||
{ name: 'dailyTask', label: '每日任务(限时)', icon: 'Trophy' },
|
||||
{ name: 'cardFlip', label: '每周邀请(限时)', icon: 'Present' },
|
||||
// { name: 'usageStatistics2', label: '用量统计2', icon: 'Histogram' },
|
||||
{ name: 'activationCode', label: '激活码兑换', icon: 'MagicStick' },
|
||||
];
|
||||
function openDialog() {
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function handleConfirm(activeNav: string) {
|
||||
ElMessage.success('操作成功');
|
||||
}
|
||||
|
||||
// 导航切换
|
||||
function handleNavChange(nav: string) {
|
||||
activeNav.value = nav;
|
||||
// 同步更新 store 中的 tab 状态,防止下次通过 store 打开同一 tab 时因值未变而不触发 watch
|
||||
if (userStore.userCenterActiveTab !== nav) {
|
||||
userStore.userCenterActiveTab = nav;
|
||||
}
|
||||
}
|
||||
|
||||
// 联系售后
|
||||
function handleContactSupport() {
|
||||
rechargeLogRef.value?.contactCustomerService();
|
||||
}
|
||||
const { startHeaderTour } = useGuideTour();
|
||||
|
||||
// 开始引导教程
|
||||
function handleStartTutorial() {
|
||||
startHeaderTour();
|
||||
}
|
||||
// 点击
|
||||
function handleClick(item: any) {
|
||||
switch (item.key) {
|
||||
case '1':
|
||||
ElMessage.warning('暂未开放');
|
||||
break;
|
||||
case '2':
|
||||
ElMessage.warning('暂未开放');
|
||||
break;
|
||||
case '5':
|
||||
// 打开控制台
|
||||
popoverRef.value?.hide?.();
|
||||
router.push('/console');
|
||||
break;
|
||||
case '6':
|
||||
handleStartTutorial();
|
||||
break;
|
||||
case '7':
|
||||
// 打开公告
|
||||
popoverRef.value?.hide?.();
|
||||
announcementStore.openDialog();
|
||||
break;
|
||||
case '8':
|
||||
// 打开模型库
|
||||
popoverRef.value?.hide?.();
|
||||
router.push('/model-library');
|
||||
break;
|
||||
case '9':
|
||||
// 打开文档
|
||||
popoverRef.value?.hide?.();
|
||||
window.open('https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde', '_blank');
|
||||
break;
|
||||
case '4':
|
||||
popoverRef.value?.hide?.();
|
||||
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
|
||||
confirmButtonText: '确认退出',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(async () => {
|
||||
// 在这里执行退出方法
|
||||
await userStore.logout();
|
||||
// 清空回话列表并回到默认页
|
||||
await sessionStore.requestSessionList(1, true);
|
||||
await sessionStore.createSessionBtn();
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '退出成功',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// ElMessage({
|
||||
// type: 'info',
|
||||
// message: '取消',
|
||||
// });
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function openVipGuide() {
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<div class="text-center leading-relaxed">
|
||||
<h3 class="text-lg font-bold mb-3">${isUserVip() ? 'YiXinAI-VIP 会员' : '成为 YiXinAI-VIP'}</h3>
|
||||
<p class="mb-2">
|
||||
${
|
||||
isUserVip()
|
||||
? '您已是尊贵会员,享受全部 AI 模型与专属服务。感谢支持!'
|
||||
: '解锁所有 AI 模型,无限加速,专属客服,尽享尊贵体验。'
|
||||
}
|
||||
</p>
|
||||
${
|
||||
isUserVip()
|
||||
? '<p class="text-sm text-gray-500">您可随时访问产品页面查看更多特权内容。</p>'
|
||||
: '<p class="text-sm text-gray-500">点击下方按钮,立即升级为 VIP 会员!</p>'
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
isUserVip() ? '会员状态' : '会员尊享',
|
||||
{
|
||||
confirmButtonText: '前往产品页面',
|
||||
cancelButtonText: '关闭',
|
||||
dangerouslyUseHTMLString: true,
|
||||
type: 'info',
|
||||
center: true,
|
||||
roundButton: true,
|
||||
},
|
||||
)
|
||||
.then(() => {
|
||||
router.push({
|
||||
name: 'products', // 使用命名路由
|
||||
query: { from: isUserVip() ? 'vip' : 'user' }, // 可选:添加来源标识
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 点击右上角关闭或“关闭”按钮,不执行任何操作
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 监听对话框打开事件,切换到邀请码标签页 ============
|
||||
watch(dialogVisible, (newVal) => {
|
||||
if (newVal && externalInviteCode.value) {
|
||||
// 对话框打开后,切换标签页(已通过 :default-active 绑定,会自动响应)
|
||||
// console.log('[Avatar] watch: 对话框已打开,切换到 cardFlip 标签页');
|
||||
nextTick(() => {
|
||||
activeNav.value = 'cardFlip';
|
||||
// console.log('[Avatar] watch: 已设置 activeNav 为', activeNav.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 对话框关闭时,清除邀请码状态和 URL 参数
|
||||
if (!newVal && externalInviteCode.value) {
|
||||
// console.log('[Avatar] watch: 对话框关闭,清除邀请码状态');
|
||||
externalInviteCode.value = '';
|
||||
|
||||
// 清除 URL 中的 inviteCode 参数
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.has('inviteCode')) {
|
||||
url.searchParams.delete('inviteCode');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
// console.log('[Avatar] watch: 已清除 URL 中的 inviteCode 参数');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听 URL 参数,实现邀请码快捷分享 ============
|
||||
onMounted(() => {
|
||||
// 获取 URL 查询参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const inviteCode = urlParams.get('inviteCode');
|
||||
|
||||
if (inviteCode && inviteCode.trim()) {
|
||||
// console.log('[Avatar] onMounted: 检测到邀请码', inviteCode);
|
||||
|
||||
// 保存邀请码
|
||||
externalInviteCode.value = inviteCode.trim();
|
||||
|
||||
// 先设置标签页为 cardFlip
|
||||
activeNav.value = 'cardFlip';
|
||||
// console.log('[Avatar] onMounted: 设置 activeNav 为', activeNav.value);
|
||||
|
||||
// 延迟打开对话框,确保状态已更新
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// console.log('[Avatar] onMounted: 打开用户中心对话框');
|
||||
dialogVisible.value = true;
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// 注意:不立即清除 URL 参数,保留给登录后使用
|
||||
// URL 参数会在对话框关闭时清除
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听引导状态,自动打开用户中心并开始引导 ============
|
||||
watch(() => guideTourStore.shouldStartUserCenterTour, (shouldStart) => {
|
||||
if (shouldStart) {
|
||||
// 清除触发标记
|
||||
guideTourStore.clearUserCenterTourTrigger();
|
||||
|
||||
// 注册导航切换回调
|
||||
guideTourStore.setUserCenterNavChangeCallback((nav: string) => {
|
||||
activeNav.value = nav;
|
||||
});
|
||||
|
||||
// 注册关闭弹窗回调
|
||||
guideTourStore.setUserCenterCloseCallback(() => {
|
||||
dialogVisible.value = false;
|
||||
});
|
||||
|
||||
// 打开用户中心弹窗
|
||||
nextTick(() => {
|
||||
dialogVisible.value = true;
|
||||
|
||||
// 等待弹窗打开后开始引导
|
||||
setTimeout(() => {
|
||||
startUserCenterTour();
|
||||
}, 600);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听 Store 状态,控制用户中心弹窗 (新增) ============
|
||||
watch(() => userStore.isUserCenterVisible, (val) => {
|
||||
dialogVisible.value = val;
|
||||
if (val && userStore.userCenterActiveTab) {
|
||||
activeNav.value = userStore.userCenterActiveTab;
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => userStore.userCenterActiveTab, (val) => {
|
||||
if (val) {
|
||||
activeNav.value = val;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听本地 dialogVisible 变化,同步回 Store(可选,为了保持一致性)
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
userStore.closeUserCenter();
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 暴露方法供外部调用 ============
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2 ">
|
||||
<!-- 用户信息区域 -->
|
||||
<div class="user-info-display cursor-pointer flex flex-col text-right mr-2 leading-tight" @click="openDialog">
|
||||
<div class="text-sm font-semibold text-gray-800">
|
||||
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
|
||||
</div>
|
||||
|
||||
<!-- 角色展示 -->
|
||||
<div>
|
||||
<span
|
||||
v-if="isUserVip()"
|
||||
class="inline-block px-2 py-0.5 text-xs text-yellow-700 bg-yellow-100 rounded-full font-semibold"
|
||||
>
|
||||
YiXinAI-VIP
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="inline-block px-2 py-0.5 text-xs text-gray-600 bg-gray-100 rounded-full cursor-pointer hover:bg-yellow-50 hover:text-yellow-700 transition"
|
||||
>
|
||||
普通用户
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 头像区域 -->
|
||||
<div class="avatar-container" data-tour="user-avatar">
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
placement="bottom-end"
|
||||
trigger="clickTarget"
|
||||
:trigger-style="{ cursor: 'pointer' }"
|
||||
popover-class="popover-content"
|
||||
:popover-style="popoverStyle"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-avatar :src="getUserProfilePicture()" :size="28" fit="fit" shape="circle" />
|
||||
</template>
|
||||
|
||||
<div class="popover-content-box shadow-lg">
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-info-box flex items-center gap-8px p-8px rounded-lg mb-2">
|
||||
<el-avatar :src="getUserProfilePicture()" :size="32" fit="fit" shape="circle" />
|
||||
<div class="flex flex-col text-sm">
|
||||
<div class="font-semibold text-gray-800">
|
||||
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
<span
|
||||
v-if="isUserVip()"
|
||||
class="inline-block px-2 py-0.5 text-xs text-yellow-700 bg-yellow-100 rounded-full font-semibold"
|
||||
>
|
||||
YiXinAI-VIP
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="inline-block px-2 py-0.5 text-xs text-gray-600 bg-gray-100 rounded-full cursor-pointer hover:bg-yellow-50 hover:text-yellow-700 transition"
|
||||
>
|
||||
普通用户
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divder h-1px bg-gray-200 my-4px" />
|
||||
|
||||
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
|
||||
<div
|
||||
v-if="!item.divider"
|
||||
class="popover-content-box-item flex items-center h-full gap-8px p-8px pl-10px pr-12px rounded-lg hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<SvgIcon :name="item.icon!" size="16" class-name="flex-none" />
|
||||
<div class="popover-content-box-item-text font-size-14px text-overflow max-h-120px">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.divider" class="divder h-1px bg-gray-200 my-4px" />
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
<nav-dialog
|
||||
v-model="dialogVisible"
|
||||
title="控制台"
|
||||
:nav-items="navItems"
|
||||
:default-active="activeNav"
|
||||
@confirm="handleConfirm"
|
||||
@nav-change="handleNavChange"
|
||||
>
|
||||
<template #extra-actions>
|
||||
<el-tooltip v-if="isUserVip() && activeNav === 'rechargeLog'" content="联系售后" placement="bottom">
|
||||
<el-button circle plain size="small" @click="handleContactSupport">
|
||||
<el-icon color="#07c160">
|
||||
<ChatLineRound />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<!-- 用户管理内容 -->
|
||||
<template #user>
|
||||
<user-management />
|
||||
</template>
|
||||
<!-- 用量统计 -->
|
||||
<template #usageStatistics>
|
||||
<usage-statistics />
|
||||
</template>
|
||||
<!-- 尊享服务 -->
|
||||
<template #premiumService>
|
||||
<premium-service />
|
||||
</template>
|
||||
<!-- 用量统计 -->
|
||||
<!-- <template #usageStatistics2> -->
|
||||
<!-- <usage-statistics2 /> -->
|
||||
<!-- </template> -->
|
||||
|
||||
<!-- 角色管理内容 -->
|
||||
<template #role>
|
||||
<!-- < /> -->
|
||||
</template>
|
||||
|
||||
<!-- 权限管理内容 -->
|
||||
<template #permission>
|
||||
<!-- <permission-management /> -->
|
||||
</template>
|
||||
|
||||
<template #apiKey>
|
||||
<APIKeyManagement />
|
||||
</template>
|
||||
<template #activationCode>
|
||||
<activation-code />
|
||||
</template>
|
||||
<template #dailyTask>
|
||||
<daily-task />
|
||||
</template>
|
||||
<template #cardFlip>
|
||||
<card-flip-activity :external-invite-code="externalInviteCode" />
|
||||
</template>
|
||||
<template #rechargeLog>
|
||||
<recharge-log ref="rechargeLogRef" />
|
||||
</template>
|
||||
</nav-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popover-content {
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
}
|
||||
.popover-content-box {
|
||||
padding: 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { showProductPackage } from '@/utils/product-package';
|
||||
|
||||
// 点击购买按钮
|
||||
function onProductPackage() {
|
||||
showProductPackage();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="buy-btn-container">
|
||||
<el-button
|
||||
class="buy-btn flex items-center gap-2 px-5 py-2 font-semibold shadow-lg"
|
||||
data-tour="buy-btn"
|
||||
@click="onProductPackage"
|
||||
>
|
||||
<span>立即购买</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.buy-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 22px 0 0;
|
||||
|
||||
.buy-btn {
|
||||
background: linear-gradient(90deg, #FFD700, #FFC107);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.5);
|
||||
background: linear-gradient(90deg, #FFC107, #FFD700);
|
||||
}
|
||||
|
||||
.icon-rocket {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 1.2s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端,屏幕小于756px
|
||||
@media screen and (max-width: 756px) {
|
||||
.buy-btn-container {
|
||||
margin: 0 ;
|
||||
|
||||
.buy-btn {
|
||||
font-size: 12px;
|
||||
max-width: 60px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- 侧边栏折叠按钮 -->
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { useDesignStore } from '@/stores';
|
||||
|
||||
// const { changeCollapse } = useCollapseToggle();
|
||||
const designStore = useDesignStore();
|
||||
|
||||
function handleChangeCollapse() {
|
||||
designStore.setIsCollapseConversationList(!designStore.isCollapseConversationList);
|
||||
// 待定
|
||||
// changeCollapse();
|
||||
// // 每次切换折叠状态,重置安全区状态
|
||||
// designStore.isSafeAreaHover = false;
|
||||
// // 重置首次激活悬停状态
|
||||
// designStore.hasActivatedHover = false;
|
||||
// if (!designStore.isCollapse) {
|
||||
// document.documentElement.style.setProperty(
|
||||
// `--sidebar-left-container-default-width`,
|
||||
// `${SIDE_BAR_WIDTH}px`,
|
||||
// );
|
||||
// }
|
||||
// else {
|
||||
// document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``);
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="collapse-container btn-icon-btn" @click="handleChangeCollapse">
|
||||
<SvgIcon v-if="!designStore.isCollapseConversationList" name="ms-left-panel-close-outline" size="24" />
|
||||
<SvgIcon v-if="designStore.isCollapseConversationList" name="ms-left-panel-open-outline" size="24" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// .collapse-container {
|
||||
// }
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 打开用户中心对话框(通过调用 Avatar 组件的方法)
|
||||
function openConsole() {
|
||||
// 触发事件,由父组件处理
|
||||
emit('open-console');
|
||||
}
|
||||
|
||||
const emit = defineEmits(['open-console']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="console-btn-container" data-tour="console-btn">
|
||||
<div
|
||||
class="console-btn"
|
||||
title="打开控制台"
|
||||
@click="openConsole"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">控制台</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||
<line x1="8" y1="21" x2="16" y2="21" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.console-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.console-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #909399;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.console-btn-container {
|
||||
.console-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<!-- 添加新会话按钮 -->
|
||||
<script setup lang="ts">
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
/* 创建会话 开始 */
|
||||
function handleCreatChat() {
|
||||
if (!sessionStore.currentSession)
|
||||
return;
|
||||
// 创建会话, 跳转到默认聊天
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
/* 创建会话 结束 */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="create-chat-container flex-center flex-none p-6px pl-8px pr-8px c-#0057ff b-#0057ff b-rounded-12px border-1px hover:bg-#0057ff hover:c-#fff hover:b-#fff hover:cursor-pointer border-solid select-none"
|
||||
:class="{
|
||||
'is-disabled': !sessionStore.currentSession,
|
||||
}"
|
||||
@click="handleCreatChat"
|
||||
>
|
||||
<el-icon size="12" class="flex-center flex-none w-14px h-14px">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span class="ml-4px font-size-14px font-700">新对话</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
color: #0057ff;
|
||||
cursor: not-allowed;
|
||||
background-color: transparent;
|
||||
border-color: #0057ff;
|
||||
border-style: solid;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- LoginBtn 登录按钮 -->
|
||||
<script setup lang="ts">
|
||||
import LoginDialog from '@/components/LoginDialog/index.vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const isLoginDialogVisible = computed(() => userStore.isLoginDialogVisible);
|
||||
|
||||
// 点击登录按钮时调用Store方法打开弹框
|
||||
function handleClickLogin() {
|
||||
userStore.openLoginDialog();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-btn-wrapper">
|
||||
<div
|
||||
class="login-btn bg-#191c1f c-#fff font-size-14px rounded-8px flex-center text-overflow p-10px pl-12px pr-12px min-w-49px h-16px cursor-pointer hover:bg-#232629 select-none"
|
||||
@click="handleClickLogin"
|
||||
>
|
||||
登录
|
||||
</div>
|
||||
|
||||
<!-- 登录弹框 -->
|
||||
<LoginDialog v-model:visible="isLoginDialogVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goToModelLibrary() {
|
||||
router.push('/model-library');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="model-library-btn-container" data-tour="model-library-btn">
|
||||
<div
|
||||
class="model-library-btn"
|
||||
title="查看模型库"
|
||||
@click="goToModelLibrary"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">模型库</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.model-library-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.model-library-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #606266;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.model-library-btn-container {
|
||||
.model-library-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 检查是否在聊天页面
|
||||
const isOnChatPage = computed(() => {
|
||||
return route.path.startsWith('/chat');
|
||||
});
|
||||
|
||||
function goToChat() {
|
||||
router.push('/chat/conversation');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!isOnChatPage" class="start-chat-btn-container" data-tour="start-chat-btn">
|
||||
<div
|
||||
class="start-chat-btn"
|
||||
title="开始聊天"
|
||||
@click="goToChat"
|
||||
>
|
||||
<el-icon class="chat-icon">
|
||||
<i-ep-chat-dot-round />
|
||||
</el-icon>
|
||||
<span class="btn-text">开始聊天</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.start-chat-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 12px;
|
||||
|
||||
.start-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.chat-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.start-chat-btn-container {
|
||||
margin-right: 8px;
|
||||
|
||||
.start-chat-btn {
|
||||
padding: 8px;
|
||||
|
||||
.btn-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core';
|
||||
|
||||
// 使用 VueUse 的 useColorMode
|
||||
const mode = useColorMode({
|
||||
attribute: 'class',
|
||||
modes: {
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
},
|
||||
});
|
||||
|
||||
// 切换主题
|
||||
function toggleTheme() {
|
||||
mode.value = mode.value === 'dark' ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
// 主题图标
|
||||
const themeIcon = computed(() => {
|
||||
return mode.value === 'dark' ? 'Sunny' : 'Moon';
|
||||
});
|
||||
|
||||
// 主题标题
|
||||
const themeTitle = computed(() => {
|
||||
return mode.value === 'dark' ? '切换到浅色模式' : '切换到深色模式';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-btn-container" data-tour="theme-btn">
|
||||
<div
|
||||
class="theme-btn"
|
||||
:title="themeTitle"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<!-- PC端显示文字 + 图标 -->
|
||||
<el-icon class="theme-icon">
|
||||
<component :is="`i-ep-${themeIcon}`" />
|
||||
</el-icon>
|
||||
<span class="pc-text">主题</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.theme-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.theme-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
font-size: 18px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
&:hover .theme-icon {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
// PC端显示文字
|
||||
.pc-text {
|
||||
display: inline;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.theme-btn-container {
|
||||
.theme-btn {
|
||||
padding: 8px;
|
||||
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<!-- 标题编辑 -->
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const currentSession = computed(() => sessionStore.currentSession);
|
||||
|
||||
function handleClickTitle() {
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: currentSession.value?.sessionTitle,
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
})
|
||||
.then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: currentSession.value!.id,
|
||||
sessionTitle: value,
|
||||
sessionContent: currentSession.value!.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
// 如果是当前会话,则更新当前选中会话信息
|
||||
sessionStore.setCurrentSession({
|
||||
...currentSession.value,
|
||||
sessionTitle: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// ElMessage({
|
||||
// type: 'info',
|
||||
// message: '取消修改',
|
||||
// });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentSession" class="w-full h-full flex flex-col justify-center">
|
||||
<div class="box-border mr-20px">
|
||||
<div
|
||||
class="title-editing-container p-4px w-fit max-w-full flex items-center justify-start cursor-pointer select-none hover:bg-[rgba(0,0,0,.04)] cursor-pointer rounded-md font-size-14px"
|
||||
@click="handleClickTitle"
|
||||
>
|
||||
<div class="text-overflow select-none pr-8px">
|
||||
{{ currentSession.sessionTitle }}
|
||||
</div>
|
||||
<SvgIcon name="draft-line" size="14" class="flex-none c-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.title-editing-container {
|
||||
transition: all 0.3s ease;
|
||||
&:hover {
|
||||
.svg-icon {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.svg-icon {
|
||||
display: none;
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { QuestionFilled } from '@element-plus/icons-vue';
|
||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||
|
||||
const { startHeaderTour } = useGuideTour();
|
||||
|
||||
// 开始引导教程
|
||||
function handleStartTutorial() {
|
||||
startHeaderTour();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tutorial-btn-container" data-tour="tutorial-btn">
|
||||
<div
|
||||
class="tutorial-btn"
|
||||
@click="handleStartTutorial"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">新手引导</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<el-icon class="mobile-icon" :size="20">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tutorial-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.tutorial-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.tutorial-btn-container {
|
||||
.tutorial-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
Yi.Ai.Vue3/src/layouts/components0/Header/index.vue
Normal file
122
Yi.Ai.Vue3/src/layouts/components0/Header/index.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<!-- Header 头部 -->
|
||||
<script setup lang="ts">
|
||||
import { onKeyStroke } from '@vueuse/core';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { SIDE_BAR_WIDTH } from '@/config/index';
|
||||
import { useDesignStore, useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
import AiTutorialBtn from './components/AiTutorialBtn.vue';
|
||||
import AnnouncementBtn from './components/AnnouncementBtn.vue';
|
||||
import Avatar from './components/Avatar.vue';
|
||||
import BuyBtn from './components/BuyBtn.vue';
|
||||
import Collapse from './components/Collapse.vue';
|
||||
import ConsoleBtn from './components/ConsoleBtn.vue';
|
||||
import CreateChat from './components/CreateChat.vue';
|
||||
import LoginBtn from './components/LoginBtn.vue';
|
||||
import ModelLibraryBtn from './components/ModelLibraryBtn.vue';
|
||||
import StartChatBtn from './components/StartChatBtn.vue';
|
||||
import ThemeBtn from './components/ThemeBtn.vue';
|
||||
import TitleEditing from './components/TitleEditing.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const avatarRef = ref();
|
||||
|
||||
const currentSession = computed(() => sessionStore.currentSession);
|
||||
|
||||
onMounted(() => {
|
||||
// 全局设置侧边栏默认宽度 (这个是不变的,一开始就设置)
|
||||
document.documentElement.style.setProperty(`--sidebar-default-width`, `${SIDE_BAR_WIDTH}px`);
|
||||
if (designStore.isCollapse) {
|
||||
document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``);
|
||||
}
|
||||
else {
|
||||
document.documentElement.style.setProperty(
|
||||
`--sidebar-left-container-default-width`,
|
||||
`${SIDE_BAR_WIDTH}px`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 定义 Ctrl+K 的处理函数
|
||||
function handleCtrlK(event: KeyboardEvent) {
|
||||
event.preventDefault(); // 防止默认行为
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 设置全局的键盘按键监听
|
||||
onKeyStroke(event => event.ctrlKey && event.key.toLowerCase() === 'k', handleCtrlK, {
|
||||
passive: false,
|
||||
});
|
||||
|
||||
// 打开控制台
|
||||
function handleOpenConsole() {
|
||||
router.push('/console');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<div class="header-box relative z-10 top-0 left-0 right-0">
|
||||
<div class="absolute left-0 right-0 top-0 bottom-0 flex items-center flex-row">
|
||||
<div
|
||||
class="overflow-hidden flex h-full items-center flex-row flex-1 w-fit flex-shrink-0 min-w-0"
|
||||
>
|
||||
<div class="w-full flex items-center flex-row">
|
||||
<!-- 左边 -->
|
||||
<div
|
||||
v-if="designStore.isCollapse"
|
||||
class="left-box flex h-full items-center pl-20px gap-12px flex-shrink-0 flex-row"
|
||||
>
|
||||
<Collapse />
|
||||
<CreateChat />
|
||||
<div v-if="currentSession" class="w-0.5px h-30px bg-[rgba(217,217,217)]" />
|
||||
</div>
|
||||
|
||||
<!-- 中间 -->
|
||||
<div class="middle-box flex-1 min-w-0 ml-12px">
|
||||
<TitleEditing />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-box flex h-full items-center pr-20px flex-shrink-0 mr-auto flex-row">
|
||||
<StartChatBtn />
|
||||
<AnnouncementBtn />
|
||||
<ModelLibraryBtn />
|
||||
<ThemeBtn />
|
||||
<AiTutorialBtn />
|
||||
<ConsoleBtn @open-console="handleOpenConsole" />
|
||||
<BuyBtn v-show="userStore.userInfo" />
|
||||
<Avatar v-show="userStore.userInfo" ref="avatarRef" />
|
||||
<LoginBtn v-show="!userStore.userInfo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
.header-box {
|
||||
width: 100%;
|
||||
width: calc(
|
||||
100% - var(--sidebar-left-container-default-width, 0px) - var(
|
||||
--sidebar-right-container-default-width,
|
||||
0px
|
||||
)
|
||||
);
|
||||
height: var(--header-container-default-heigth);
|
||||
margin: 0 var(--sidebar-right-container-default-width, 0) 0
|
||||
var(--sidebar-left-container-default-width, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
8
Yi.Ai.Vue3/src/layouts/components0/Logo/index.vue
Normal file
8
Yi.Ai.Vue3/src/layouts/components0/Logo/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- Logo -->
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>Logo</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
89
Yi.Ai.Vue3/src/layouts/components0/Main/index.vue
Normal file
89
Yi.Ai.Vue3/src/layouts/components0/Main/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<!-- Main -->
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import { useKeepAliveStore } from '@/stores/modules/keepAlive';
|
||||
|
||||
const designStore = useDesignStore();
|
||||
const keepAliveStore = useKeepAliveStore();
|
||||
const useroute = useRoute();
|
||||
|
||||
const transitionName = computed(() => {
|
||||
if (useroute.meta.isDefaultChat) {
|
||||
return 'slide';
|
||||
}
|
||||
else {
|
||||
return designStore.pageAnimateType;
|
||||
}
|
||||
});
|
||||
|
||||
// 刷新当前路由页面缓存方法
|
||||
const isRouterShow = ref(true);
|
||||
const refreshMainPage = (val: boolean) => (isRouterShow.value = val);
|
||||
provide('refresh', refreshMainPage);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-main
|
||||
class="layout-main"
|
||||
:class="{ 'layout-main-overfow-hidden': useroute.meta.isDefaultChat }"
|
||||
>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition :name="transitionName" mode="out-in" appear>
|
||||
<keep-alive :max="10" :include="keepAliveStore.keepAliveName">
|
||||
<component :is="Component" v-if="isRouterShow" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-main-overfow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 默认聊天页面:上下滑动动画 */
|
||||
.slide-enter-from {
|
||||
margin-top: 200px;
|
||||
opacity: 0;
|
||||
}
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.3s; /* 缓出动画 */
|
||||
}
|
||||
.slide-enter-to {
|
||||
margin-top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
.slide-leave-from {
|
||||
margin-top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
.slide-leave-to {
|
||||
margin-top: 200px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 带id聊天页面:中间缩放动画 */
|
||||
// .zoom-fade-enter-from {
|
||||
// transform: scale(0.9); /* 进入前:缩小隐藏 */
|
||||
// opacity: 0;
|
||||
// }
|
||||
// .zoom-fade-enter-active,
|
||||
// .zoom-fade-leave-active {
|
||||
// transition: all 0.3s; /* 缓入动画 */
|
||||
// }
|
||||
// .zoom-fade-enter-to {
|
||||
// transform: scale(1); /* 进入后:正常大小 */
|
||||
// opacity: 1;
|
||||
// }
|
||||
// .zoom-fade-leave-from {
|
||||
// transform: scale(1); /* 离开前:正常大小 */
|
||||
// opacity: 1;
|
||||
// }
|
||||
// .zoom-fade-leave-to {
|
||||
// transform: scale(0.9); /* 离开后:缩小隐藏 */
|
||||
// opacity: 0;
|
||||
// }
|
||||
</style>
|
||||
@@ -1,21 +1,41 @@
|
||||
<!-- 主布局 -->
|
||||
<script setup lang="ts">
|
||||
import type { LayoutType } from '@/config/design';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useResponsive } from '@/hooks/useResponsive';
|
||||
import LayoutBlankPage from '@/layouts/LayoutBlankPage/index.vue';
|
||||
// import { useScreenStore } from '@/hooks/useScreen';
|
||||
import LayoutDefault from '@/layouts/LayoutDefault/index.vue';
|
||||
import LayoutMobile from '@/layouts/LayoutMobile/index.vue';
|
||||
import LayoutVertical from '@/layouts/LayoutVertical/index.vue';
|
||||
import { useDesignStore } from '@/stores';
|
||||
|
||||
// 这里添加布局类型
|
||||
const LayoutComponent: Record<LayoutType, Component> = {
|
||||
const LayoutComponent: Record<LayoutType | 'mobile', Component> = {
|
||||
default: LayoutDefault,
|
||||
vertical: LayoutVertical,
|
||||
blankPage: LayoutBlankPage,
|
||||
mobile: LayoutMobile,
|
||||
|
||||
};
|
||||
|
||||
const designStore = useDesignStore();
|
||||
// const { isMobile } = useScreenStore();
|
||||
const { isMobile } = useResponsive();
|
||||
const route = useRoute();
|
||||
|
||||
/** 获取布局格式 */
|
||||
const layout = computed((): LayoutType => designStore.layout);
|
||||
const layout = computed((): LayoutType | 'mobile' => {
|
||||
// 移动端强制使用移动布局
|
||||
// if (isMobile.value) {
|
||||
// return 'mobile';
|
||||
// }
|
||||
// 优先使用路由 meta 中定义的 layout
|
||||
if (route.meta?.layout) {
|
||||
return route.meta.layout as LayoutType;
|
||||
}
|
||||
// 否则使用全局设置的 layout
|
||||
return designStore.layout;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 更好的做法是等待所有资源加载
|
||||
window.addEventListener('load', () => {
|
||||
@@ -47,4 +67,23 @@ onMounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
:deep(.aside-content .conversations-container){
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.aside-content .conversations-list){
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.aside-content .conversations-wrap){
|
||||
padding: 0 !important;
|
||||
}
|
||||
:deep(.aside-content .el-scrollbar__thumb){
|
||||
width: 0 !important;
|
||||
}
|
||||
:deep(.nav-menu)
|
||||
{
|
||||
border-right: 0.5px solid var(--s-color-border-tertiary, rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
</style>
|
||||
|
||||
26
Yi.Ai.Vue3/src/pages/chat/agent/index.vue
Normal file
26
Yi.Ai.Vue3/src/pages/chat/agent/index.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
// 智能体功能 - 预留
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image-generation-page">
|
||||
<el-empty description="智能体功能开发中,敬请期待">
|
||||
<template #image>
|
||||
<el-icon style="font-size: 80px; color: var(--el-color-primary);">
|
||||
<i-ep-picture />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.image-generation-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user