Compare commits
39 Commits
ai-agent-b
...
ai-agent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67cb142c07 | ||
|
|
f164b7dccc | ||
|
|
6cc14c1e32 | ||
|
|
e992cfc928 | ||
|
|
42edd4c230 | ||
|
|
3892ff1937 | ||
|
|
12878ba022 | ||
|
|
a3259ad36f | ||
|
|
5bb7dfb7cd | ||
|
|
3447e2dc5d | ||
|
|
88fae0cdc2 | ||
|
|
5a7f0ab108 | ||
|
|
be5f57f654 | ||
|
|
a6e7a5e906 | ||
|
|
c4ab176089 | ||
|
|
a50f877964 | ||
|
|
28cdc29369 | ||
|
|
e39cbaf5e7 | ||
|
|
9d1dd72584 | ||
|
|
ea403fcae0 | ||
|
|
91533909c2 | ||
|
|
61d5d40dbb | ||
|
|
38dbd0aca7 | ||
|
|
343347ea11 | ||
|
|
a9e11d161c | ||
|
|
d25ca6dc4a | ||
|
|
ba95d1798f | ||
|
|
436b5b910c | ||
|
|
560a76558a | ||
|
|
3f53eb14ab | ||
|
|
e46044e217 | ||
|
|
9c842ab802 | ||
|
|
b8c0f9a212 | ||
|
|
6cc0059691 | ||
|
|
33d28a8cb0 | ||
|
|
e4621d9049 | ||
|
|
c649ad31c2 | ||
|
|
50fc8c5f0a | ||
|
|
19ea76bd60 |
@@ -15,7 +15,7 @@ public class AgentSendInput
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// api密钥Id
|
/// api密钥Id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Token { get; set; }
|
public Guid TokenId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 模型id
|
/// 模型id
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ImageGenerationInput
|
public class ImageGenerationInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 密钥id
|
||||||
|
/// </summary>
|
||||||
|
public Guid? TokenId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 提示词
|
/// 提示词
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -16,7 +21,7 @@ public class ImageGenerationInput
|
|||||||
public string ModelId { get; set; } = string.Empty;
|
public string ModelId { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 参考图Base64列表(可选,包含前缀如 data:image/png;base64,...)
|
/// 参考图PrefixBase64列表(可选,包含前缀如 data:image/png;base64,...)
|
||||||
/// </summary>
|
/// </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.AiHub.Domain.Shared.Enums;
|
||||||
|
using Yi.Framework.Ddd.Application.Contracts;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 图片任务分页查询输入
|
/// 图片任务分页查询输入
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ImageTaskPageInput
|
public class ImageMyTaskPageInput: PagedAllResultRequestDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 页码(从1开始)
|
/// 提示词
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PageIndex { get; set; } = 1;
|
public string? Prompt { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 每页数量
|
|
||||||
/// </summary>
|
|
||||||
public int PageSize { get; set; } = 10;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 任务状态筛选(可选)
|
/// 任务状态筛选(可选)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TaskStatusEnum? TaskStatus { get; set; }
|
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;
|
public string Prompt { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 参考图Base64列表
|
/// 是否匿名
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string>? ReferenceImagesBase64 { get; set; }
|
public bool IsAnonymous { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 参考图URL列表
|
|
||||||
/// </summary>
|
|
||||||
public List<string>? ReferenceImagesUrl { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 生成图片Base64(包含前缀)
|
|
||||||
/// </summary>
|
|
||||||
public string? StoreBase64 { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成图片URL
|
/// 生成图片URL
|
||||||
@@ -42,8 +32,35 @@ public class ImageTaskOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TaskStatusEnum TaskStatus { get; set; }
|
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>
|
||||||
/// 创建时间
|
/// 创建时间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime CreationTime { get; set; }
|
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
|
/// 模型ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 模型分类
|
|
||||||
/// </summary>
|
|
||||||
public string Category { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 模型id
|
/// 模型id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -28,36 +22,6 @@ public class ModelGetListOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ModelDescribe { get; set; }
|
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>
|
||||||
/// 备注信息
|
/// 备注信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -30,32 +30,94 @@ public class ImageGenerationJob : AsyncBackgroundJob<ImageGenerationJobArgs>, IT
|
|||||||
|
|
||||||
public override async Task ExecuteAsync(ImageGenerationJobArgs args)
|
public override async Task ExecuteAsync(ImageGenerationJobArgs args)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("开始执行图片生成任务,TaskId: {TaskId}, ModelId: {ModelId}, UserId: {UserId}",
|
var task = await _imageStoreTaskRepository.GetFirstAsync(x => x.Id == args.TaskId);
|
||||||
args.TaskId, args.ModelId, args.UserId);
|
if (task is null)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException($"{args.TaskId} 图片生成任务不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("开始执行图片生成任务,TaskId: {TaskId}, ModelId: {ModelId}, UserId: {UserId}",
|
||||||
|
task.Id, task.ModelId, task.UserId);
|
||||||
try
|
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(
|
await _aiGateWayManager.GeminiGenerateContentImageForStatisticsAsync(
|
||||||
args.TaskId,
|
task.Id,
|
||||||
args.ModelId,
|
task.ModelId,
|
||||||
request,
|
request,
|
||||||
args.UserId);
|
task.UserId,
|
||||||
|
tokenId: task.TokenId);
|
||||||
|
|
||||||
|
|
||||||
_logger.LogInformation("图片生成任务完成,TaskId: {TaskId}", args.TaskId);
|
_logger.LogInformation("图片生成任务完成,TaskId: {TaskId}", args.TaskId);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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);
|
||||||
|
|
||||||
// 更新任务状态为失败
|
task.TaskStatus = TaskStatusEnum.Fail;
|
||||||
var task = await _imageStoreTaskRepository.GetFirstAsync(x => x.Id == args.TaskId);
|
task.ErrorInfo = error;
|
||||||
if (task != null)
|
|
||||||
{
|
await _imageStoreTaskRepository.UpdateAsync(task);
|
||||||
task.TaskStatus = TaskStatusEnum.Fail;
|
|
||||||
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
|
/// 图片任务ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid TaskId { get; set; }
|
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; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Yi.Framework.AiHub.Application.Services;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 渠道商管理服务实现
|
/// 渠道商管理服务实现
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize]
|
[Authorize(Roles = "admin")]
|
||||||
public class ChannelService : ApplicationService, IChannelService
|
public class ChannelService : ApplicationService, IChannelService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarRepository<AiAppAggregateRoot, Guid> _appRepository;
|
private readonly ISqlSugarRepository<AiAppAggregateRoot, Guid> _appRepository;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public class AiChatService : ApplicationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取模型列表
|
/// 获取对话模型列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<List<ModelGetListOutput>> GetModelAsync()
|
public async Task<List<ModelGetListOutput>> GetModelAsync()
|
||||||
@@ -100,16 +100,9 @@ public class AiChatService : ApplicationService
|
|||||||
.Select(x => new ModelGetListOutput
|
.Select(x => new ModelGetListOutput
|
||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
Category = "chat",
|
|
||||||
ModelId = x.ModelId,
|
ModelId = x.ModelId,
|
||||||
ModelName = x.Name,
|
ModelName = x.Name,
|
||||||
ModelDescribe = x.Description,
|
ModelDescribe = x.Description,
|
||||||
ModelPrice = 0,
|
|
||||||
ModelType = "1",
|
|
||||||
ModelShow = "0",
|
|
||||||
SystemPrompt = null,
|
|
||||||
ApiHost = null,
|
|
||||||
ApiKey = null,
|
|
||||||
Remark = x.Description,
|
Remark = x.Description,
|
||||||
IsPremiumPackage = x.IsPremium
|
IsPremiumPackage = x.IsPremium
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
@@ -209,7 +202,7 @@ public class AiChatService : ApplicationService
|
|||||||
[HttpPost("ai-chat/agent/send")]
|
[HttpPost("ai-chat/agent/send")]
|
||||||
public async Task PostAgentSendAsync([FromBody] AgentSendInput input, CancellationToken cancellationToken)
|
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);
|
await _aiBlacklistManager.VerifiyAiBlacklist(tokenValidation.UserId);
|
||||||
// 验证用户是否为VIP
|
// 验证用户是否为VIP
|
||||||
@@ -241,7 +234,7 @@ public class AiChatService : ApplicationService
|
|||||||
await _chatManager.AgentCompleteChatStreamAsync(_httpContextAccessor.HttpContext,
|
await _chatManager.AgentCompleteChatStreamAsync(_httpContextAccessor.HttpContext,
|
||||||
input.SessionId,
|
input.SessionId,
|
||||||
input.Content,
|
input.Content,
|
||||||
input.Token,
|
tokenValidation.Token,
|
||||||
tokenValidation.TokenId,
|
tokenValidation.TokenId,
|
||||||
input.ModelId,
|
input.ModelId,
|
||||||
tokenValidation.UserId,
|
tokenValidation.UserId,
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using SqlSugar;
|
||||||
using Volo.Abp;
|
using Volo.Abp;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.BackgroundJobs;
|
using Volo.Abp.BackgroundJobs;
|
||||||
using Volo.Abp.Guids;
|
using Volo.Abp.Guids;
|
||||||
using Volo.Abp.Users;
|
using Volo.Abp.Users;
|
||||||
|
using Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||||
using Yi.Framework.AiHub.Application.Jobs;
|
using Yi.Framework.AiHub.Application.Jobs;
|
||||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
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.Extensions;
|
||||||
using Yi.Framework.AiHub.Domain.Managers;
|
using Yi.Framework.AiHub.Domain.Managers;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||||
@@ -32,6 +35,8 @@ public class AiImageService : ApplicationService
|
|||||||
private readonly ModelManager _modelManager;
|
private readonly ModelManager _modelManager;
|
||||||
private readonly IGuidGenerator _guidGenerator;
|
private readonly IGuidGenerator _guidGenerator;
|
||||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||||
|
private readonly TokenManager _tokenManager;
|
||||||
|
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||||
|
|
||||||
public AiImageService(
|
public AiImageService(
|
||||||
ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageTaskRepository,
|
ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageTaskRepository,
|
||||||
@@ -40,7 +45,8 @@ public class AiImageService : ApplicationService
|
|||||||
PremiumPackageManager premiumPackageManager,
|
PremiumPackageManager premiumPackageManager,
|
||||||
ModelManager modelManager,
|
ModelManager modelManager,
|
||||||
IGuidGenerator guidGenerator,
|
IGuidGenerator guidGenerator,
|
||||||
IWebHostEnvironment webHostEnvironment)
|
IWebHostEnvironment webHostEnvironment, TokenManager tokenManager,
|
||||||
|
ISqlSugarRepository<AiModelEntity> aiModelRepository)
|
||||||
{
|
{
|
||||||
_imageTaskRepository = imageTaskRepository;
|
_imageTaskRepository = imageTaskRepository;
|
||||||
_backgroundJobManager = backgroundJobManager;
|
_backgroundJobManager = backgroundJobManager;
|
||||||
@@ -49,6 +55,8 @@ public class AiImageService : ApplicationService
|
|||||||
_modelManager = modelManager;
|
_modelManager = modelManager;
|
||||||
_guidGenerator = guidGenerator;
|
_guidGenerator = guidGenerator;
|
||||||
_webHostEnvironment = webHostEnvironment;
|
_webHostEnvironment = webHostEnvironment;
|
||||||
|
_tokenManager = tokenManager;
|
||||||
|
_aiModelRepository = aiModelRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -65,6 +73,13 @@ public class AiImageService : ApplicationService
|
|||||||
// 黑名单校验
|
// 黑名单校验
|
||||||
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||||
|
|
||||||
|
//校验token
|
||||||
|
if (input.TokenId is not null)
|
||||||
|
{
|
||||||
|
await _tokenManager.ValidateTokenAsync(input.TokenId, input.ModelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// VIP校验
|
// VIP校验
|
||||||
if (!CurrentUser.IsAiVip())
|
if (!CurrentUser.IsAiVip())
|
||||||
{
|
{
|
||||||
@@ -86,32 +101,23 @@ public class AiImageService : ApplicationService
|
|||||||
var task = new ImageStoreTaskAggregateRoot
|
var task = new ImageStoreTaskAggregateRoot
|
||||||
{
|
{
|
||||||
Prompt = input.Prompt,
|
Prompt = input.Prompt,
|
||||||
ReferenceImagesBase64 = input.ReferenceImagesBase64 ?? new List<string>(),
|
ReferenceImagesPrefixBase64 = input.ReferenceImagesPrefixBase64 ?? new List<string>(),
|
||||||
ReferenceImagesUrl = new List<string>(),
|
ReferenceImagesUrl = new List<string>(),
|
||||||
TaskStatus = TaskStatusEnum.Processing,
|
TaskStatus = TaskStatusEnum.Processing,
|
||||||
UserId = userId
|
UserId = userId,
|
||||||
|
UserName = CurrentUser.UserName,
|
||||||
|
TokenId = input.TokenId,
|
||||||
|
ModelId = input.ModelId
|
||||||
};
|
};
|
||||||
|
|
||||||
await _imageTaskRepository.InsertAsync(task);
|
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
|
await _backgroundJobManager.EnqueueAsync(new ImageGenerationJobArgs
|
||||||
{
|
{
|
||||||
TaskId = taskId,
|
TaskId = task.Id,
|
||||||
ModelId = input.ModelId,
|
|
||||||
RequestJson = requestJson,
|
|
||||||
UserId = userId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return taskId;
|
return task.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -134,12 +140,15 @@ public class AiImageService : ApplicationService
|
|||||||
{
|
{
|
||||||
Id = task.Id,
|
Id = task.Id,
|
||||||
Prompt = task.Prompt,
|
Prompt = task.Prompt,
|
||||||
ReferenceImagesBase64 = task.ReferenceImagesBase64,
|
// ReferenceImagesBase64 = task.ReferenceImagesBase64,
|
||||||
ReferenceImagesUrl = task.ReferenceImagesUrl,
|
// ReferenceImagesUrl = task.ReferenceImagesUrl,
|
||||||
StoreBase64 = task.StoreBase64,
|
// StoreBase64 = task.StoreBase64,
|
||||||
StoreUrl = task.StoreUrl,
|
StoreUrl = task.StoreUrl,
|
||||||
TaskStatus = task.TaskStatus,
|
TaskStatus = task.TaskStatus,
|
||||||
CreationTime = task.CreationTime
|
PublishStatus = task.PublishStatus,
|
||||||
|
Categories = task.Categories,
|
||||||
|
CreationTime = task.CreationTime,
|
||||||
|
ErrorInfo = task.ErrorInfo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +158,7 @@ public class AiImageService : ApplicationService
|
|||||||
/// <param name="base64Data">Base64图片数据(包含前缀如 data:image/png;base64,)</param>
|
/// <param name="base64Data">Base64图片数据(包含前缀如 data:image/png;base64,)</param>
|
||||||
/// <returns>图片访问URL</returns>
|
/// <returns>图片访问URL</returns>
|
||||||
[HttpPost("ai-image/upload-base64")]
|
[HttpPost("ai-image/upload-base64")]
|
||||||
|
[AllowAnonymous]
|
||||||
public async Task<string> UploadBase64ToUrlAsync([FromBody] string base64Data)
|
public async Task<string> UploadBase64ToUrlAsync([FromBody] string base64Data)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(base64Data))
|
if (string.IsNullOrWhiteSpace(base64Data))
|
||||||
@@ -171,6 +181,7 @@ public class AiImageService : ApplicationService
|
|||||||
{
|
{
|
||||||
mimeType = header.Split(':')[1].Split(';')[0];
|
mimeType = header.Split(':')[1].Split(';')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
base64Content = parts[1];
|
base64Content = parts[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,57 +208,166 @@ public class AiImageService : ApplicationService
|
|||||||
throw new UserFriendlyException("Base64格式无效");
|
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))
|
if (!Directory.Exists(uploadPath))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(uploadPath);
|
Directory.CreateDirectory(uploadPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成文件名并保存
|
// 保存文件
|
||||||
var fileId = _guidGenerator.Create();
|
var fileId = _guidGenerator.Create();
|
||||||
var fileName = $"{fileId}{extension}";
|
var fileName = $"{fileId}{extension}";
|
||||||
var filePath = Path.Combine(uploadPath, fileName);
|
var filePath = Path.Combine(uploadPath, fileName);
|
||||||
|
|
||||||
await File.WriteAllBytesAsync(filePath, imageBytes);
|
await File.WriteAllBytesAsync(filePath, imageBytes);
|
||||||
|
|
||||||
// 返回访问URL
|
// 返回包含日期目录的访问URL
|
||||||
return $"/ai-images/{fileName}";
|
return $"/wwwroot/ai-images/{dateFolder}/{fileName}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询任务列表
|
/// 分页查询我的任务列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">分页查询参数</param>
|
[HttpGet("ai-image/my-tasks")]
|
||||||
/// <returns>任务列表</returns>
|
public async Task<PagedResult<ImageTaskOutput>> GetMyTaskPageAsync([FromQuery] ImageMyTaskPageInput input)
|
||||||
[HttpGet("ai-image/tasks")]
|
|
||||||
public async Task<PagedResult<ImageTaskOutput>> GetTaskPageAsync([FromQuery] ImageTaskPageInput input)
|
|
||||||
{
|
{
|
||||||
var userId = CurrentUser.GetId();
|
var userId = CurrentUser.GetId();
|
||||||
|
|
||||||
var query = _imageTaskRepository._DbQueryable
|
RefAsync<int> total = 0;
|
||||||
|
var output = await _imageTaskRepository._DbQueryable
|
||||||
.Where(x => x.UserId == userId)
|
.Where(x => x.UserId == userId)
|
||||||
.WhereIF(input.TaskStatus.HasValue, x => x.TaskStatus == input.TaskStatus!.Value)
|
.WhereIF(input.TaskStatus is not null, x => x.TaskStatus == input.TaskStatus)
|
||||||
.OrderByDescending(x => x.CreationTime);
|
.WhereIF(!string.IsNullOrWhiteSpace(input.Prompt), x => x.Prompt.Contains(input.Prompt))
|
||||||
|
.WhereIF(input.PublishStatus is not null, x => x.PublishStatus == input.PublishStatus)
|
||||||
var total = await query.CountAsync();
|
.WhereIF(input.StartTime is not null && input.EndTime is not null,
|
||||||
var items = await query
|
x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime)
|
||||||
.Skip((input.PageIndex - 1) * input.PageSize)
|
.OrderByDescending(x => x.CreationTime)
|
||||||
.Take(input.PageSize)
|
|
||||||
.Select(x => new ImageTaskOutput
|
.Select(x => new ImageTaskOutput
|
||||||
{
|
{
|
||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
Prompt = x.Prompt,
|
Prompt = x.Prompt,
|
||||||
ReferenceImagesBase64 = x.ReferenceImagesBase64,
|
|
||||||
ReferenceImagesUrl = x.ReferenceImagesUrl,
|
|
||||||
StoreBase64 = x.StoreBase64,
|
|
||||||
StoreUrl = x.StoreUrl,
|
StoreUrl = x.StoreUrl,
|
||||||
TaskStatus = x.TaskStatus,
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,4 +392,4 @@ public class PagedResult<T>
|
|||||||
Total = total;
|
Total = total;
|
||||||
Items = items;
|
Items = items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ public class TokenService : ApplicationService
|
|||||||
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
||||||
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
||||||
private readonly ModelManager _modelManager;
|
private readonly ModelManager _modelManager;
|
||||||
|
|
||||||
public TokenService(
|
public TokenService(
|
||||||
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
|
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
|
||||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
|
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
|
||||||
@@ -90,7 +91,7 @@ public class TokenService : ApplicationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("token/select-list")]
|
[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 userId = CurrentUser.GetId();
|
||||||
var tokens = await _tokenRepository._DbQueryable
|
var tokens = await _tokenRepository._DbQueryable
|
||||||
@@ -103,13 +104,17 @@ public class TokenService : ApplicationService
|
|||||||
Name = x.Name,
|
Name = x.Name,
|
||||||
IsDisabled = x.IsDisabled
|
IsDisabled = x.IsDisabled
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
|
|
||||||
tokens.Insert(0,new TokenSelectListOutputDto
|
if (includeDefault == true)
|
||||||
{
|
{
|
||||||
TokenId = Guid.Empty,
|
tokens.Insert(0, new TokenSelectListOutputDto
|
||||||
Name = "默认",
|
{
|
||||||
IsDisabled = false
|
TokenId = Guid.Empty,
|
||||||
});
|
Name = "默认",
|
||||||
|
IsDisabled = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public class PremiumPackageConst
|
|||||||
"gemini-3-pro-high",
|
"gemini-3-pro-high",
|
||||||
"gemini-3-pro-image-preview",
|
"gemini-3-pro-image-preview",
|
||||||
"gpt-5.2-codex-xhigh",
|
"gpt-5.2-codex-xhigh",
|
||||||
|
"gpt-5.2-codex",
|
||||||
|
|
||||||
"glm-4.7",
|
"glm-4.7",
|
||||||
"yi-claude-sonnet-4-5-20250929",
|
"yi-claude-sonnet-4-5-20250929",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ public static class GeminiGenerateContentAcquirer
|
|||||||
+ usage.Value.GetPath("thoughtsTokenCount").GetInt()
|
+ usage.Value.GetPath("thoughtsTokenCount").GetInt()
|
||||||
+ usage.Value.GetPath("toolUsePromptTokenCount").GetInt();
|
+ usage.Value.GetPath("toolUsePromptTokenCount").GetInt();
|
||||||
|
|
||||||
|
|
||||||
return new ThorUsageResponse
|
return new ThorUsageResponse
|
||||||
{
|
{
|
||||||
PromptTokens = inputTokens,
|
PromptTokens = inputTokens,
|
||||||
@@ -32,14 +31,47 @@ public static class GeminiGenerateContentAcquirer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取图片url,包含前缀
|
/// 获取图片 base64(包含 data:image 前缀)
|
||||||
|
/// 优先从 inlineData.data 中获取,其次从 markdown text 中解析
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="response"></param>
|
public static string GetImagePrefixBase64(JsonElement response)
|
||||||
/// <returns></returns>
|
|
||||||
public static string GetImageBase64(JsonElement response)
|
|
||||||
{
|
{
|
||||||
//todo
|
// Step 1: 优先尝试从 candidates[0].content.parts[0].inlineData.data 获取
|
||||||
//获取他的base64字符串
|
var inlineBase64 = response
|
||||||
return string.Empty;
|
.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,31 @@ public enum ActivationCodeGoodsTypeEnum
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[ActivationCodeGoods(price: 0, tokenAmount: 100000, vipMonths: 0, isCombo: false, isReusable: true,
|
[ActivationCodeGoods(price: 0, tokenAmount: 100000, vipMonths: 0, isCombo: false, isReusable: true,
|
||||||
isSameTypeOnce: false, displayName: "10w 尊享Token", content: "免费包")]
|
isSameTypeOnce: false, displayName: "10w 尊享Token", content: "免费包")]
|
||||||
Premium10WFree = 100
|
Premium10WFree = 100,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 0【888w 尊享Token】活动包
|
||||||
|
/// </summary>
|
||||||
|
[ActivationCodeGoods(price: 0, tokenAmount: 8880000, vipMonths: 0, isCombo: false, isReusable: false,
|
||||||
|
isSameTypeOnce: false, displayName: "888w 尊享Token", content: "888w活动赠送福利包")]
|
||||||
|
Premium888WFree = 200,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 0【666w 尊享Token】活动包
|
||||||
|
/// </summary>
|
||||||
|
[ActivationCodeGoods(price: 0, tokenAmount: 6660000, vipMonths: 0, isCombo: false, isReusable: false,
|
||||||
|
isSameTypeOnce: false, displayName: "666w 尊享Token", content: "666w活动赠送福利包")]
|
||||||
|
Premium666WFree = 201,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 0【100w 尊享Token】活动包
|
||||||
|
/// </summary>
|
||||||
|
[ActivationCodeGoods(price: 0, tokenAmount: 1000000, vipMonths: 0, isCombo: false, isReusable: false,
|
||||||
|
isSameTypeOnce: false, displayName: "100w 尊享Token", content: "100w活动赠送福利包")]
|
||||||
|
Premium100WFree = 202,
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ActivationCodeGoodsTypeEnumExtensions
|
public static class ActivationCodeGoodsTypeEnumExtensions
|
||||||
|
|||||||
@@ -110,10 +110,23 @@ public enum GoodsTypeEnum
|
|||||||
PremiumPackage5000W = 101,
|
PremiumPackage5000W = 101,
|
||||||
|
|
||||||
[Price(248.9, 0, 3500)]
|
[Price(248.9, 0, 3500)]
|
||||||
[DisplayName("YiXinPremiumPackage 10000W Tokens", "1亿Tokens(推荐)", "极致性价比")]
|
[DisplayName("YiXinPremiumPackage 10000W Tokens", "1亿Tokens", "极致性价比")]
|
||||||
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
|
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
|
||||||
[TokenAmount(100000000)]
|
[TokenAmount(100000000)]
|
||||||
PremiumPackage10000W = 102,
|
PremiumPackage10000W = 102,
|
||||||
|
|
||||||
|
|
||||||
|
[Price(238.9, 0, 3500)]
|
||||||
|
[DisplayName("YiXinPremiumPackage 10000W Tokens", "1亿Tokens(2026元旦限购)", "活动9.5折特价")]
|
||||||
|
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
|
||||||
|
[TokenAmount(100000000)]
|
||||||
|
PremiumPackage10000W_2026 = 103,
|
||||||
|
|
||||||
|
[Price(398.9, 0, 7000)]
|
||||||
|
[DisplayName("YiXinPremiumPackage 20000W Tokens", "2亿Tokens(2026元旦限购)", "史上最低8.8折")]
|
||||||
|
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
|
||||||
|
[TokenAmount(200000000)]
|
||||||
|
PremiumPackage20000W_2026 = 104,
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GoodsTypeEnumExtensions
|
public static class GoodsTypeEnumExtensions
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布状态枚举
|
||||||
|
/// </summary>
|
||||||
|
public enum PublishStatusEnum
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 未发布
|
||||||
|
/// </summary>
|
||||||
|
Unpublished = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已发布
|
||||||
|
/// </summary>
|
||||||
|
Published = 1
|
||||||
|
}
|
||||||
@@ -14,23 +14,18 @@ public class ImageStoreTaskAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
public string Prompt { get; set; }
|
public string Prompt { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 参考图Base64
|
/// 参考图PrefixBase64(带前缀,如 data:image/png;base64,xxx)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SugarColumn(IsJson = true)]
|
[SugarColumn(IsJson = true, ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||||
public List<string> ReferenceImagesBase64 { get; set; }
|
public List<string> ReferenceImagesPrefixBase64 { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 参考图url
|
/// 参考图url
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SugarColumn(IsJson = true)]
|
[SugarColumn(IsJson = true)]
|
||||||
public List<string> ReferenceImagesUrl { get; set; }
|
public List<string> ReferenceImagesUrl { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图片base64
|
|
||||||
/// </summary>
|
|
||||||
public string? StoreBase64 { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 图片绝对路径
|
/// 图片绝对路径
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -46,6 +41,43 @@ public class ImageStoreTaskAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid UserId { get; set; }
|
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>
|
||||||
/// 设置成功
|
/// 设置成功
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -55,4 +87,18 @@ public class ImageStoreTaskAggregateRoot : FullAuditedAggregateRoot<Guid>
|
|||||||
TaskStatus = TaskStatusEnum.Success;
|
TaskStatus = TaskStatusEnum.Success;
|
||||||
StoreUrl = storeUrl;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ public class AiGateWayManager : DomainService
|
|||||||
{
|
{
|
||||||
throw new UserFriendlyException($"【{modelId}】模型当前版本【{modelApiType}】格式不支持");
|
throw new UserFriendlyException($"【{modelId}】模型当前版本【{modelApiType}】格式不支持");
|
||||||
}
|
}
|
||||||
// ✅ 统一处理 -nx 后缀(网关层模型规范化)
|
// ✅ 统一处理 yi- 后缀(网关层模型规范化)
|
||||||
if (!string.IsNullOrEmpty(aiModelDescribe.ModelId) &&
|
if (!string.IsNullOrEmpty(aiModelDescribe.ModelId) &&
|
||||||
aiModelDescribe.ModelId.StartsWith("yi-", StringComparison.OrdinalIgnoreCase))
|
aiModelDescribe.ModelId.StartsWith("yi-", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -976,7 +976,7 @@ public class AiGateWayManager : DomainService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const string ImageStoreHost = "http://localhost:19001/api/app";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gemini 生成(Image)-非流式-缓存处理
|
/// Gemini 生成(Image)-非流式-缓存处理
|
||||||
/// 返回图片绝对路径
|
/// 返回图片绝对路径
|
||||||
@@ -1005,16 +1005,16 @@ public class AiGateWayManager : DomainService
|
|||||||
var data = await chatService.GenerateContentAsync(modelDescribe, request, cancellationToken);
|
var data = await chatService.GenerateContentAsync(modelDescribe, request, cancellationToken);
|
||||||
|
|
||||||
//解析json,获取base64字符串
|
//解析json,获取base64字符串
|
||||||
var imageBase64 = GeminiGenerateContentAcquirer.GetImageBase64(data);
|
var imagePrefixBase64 = GeminiGenerateContentAcquirer.GetImagePrefixBase64(data);
|
||||||
|
|
||||||
//远程调用上传接口,将base64转换为URL
|
//远程调用上传接口,将base64转换为URL
|
||||||
var httpClient = LazyServiceProvider.LazyGetRequiredService<IHttpClientFactory>().CreateClient();
|
var httpClient = LazyServiceProvider.LazyGetRequiredService<IHttpClientFactory>().CreateClient();
|
||||||
var uploadUrl = $"https://ccnetcore.com/prod-api/ai-hub/ai-image/upload-base64";
|
// 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 = $"{ImageStoreHost}/ai-image/upload-base64";
|
||||||
|
var content = new StringContent(JsonSerializer.Serialize(imagePrefixBase64), Encoding.UTF8, "application/json");
|
||||||
var uploadResponse = await httpClient.PostAsync(uploadUrl, content, cancellationToken);
|
var uploadResponse = await httpClient.PostAsync(uploadUrl, content, cancellationToken);
|
||||||
uploadResponse.EnsureSuccessStatusCode();
|
uploadResponse.EnsureSuccessStatusCode();
|
||||||
var storeUrl = await uploadResponse.Content.ReadAsStringAsync(cancellationToken);
|
var storeUrl = await uploadResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||||
storeUrl = storeUrl.Trim('"'); // 移除JSON字符串的引号
|
|
||||||
|
|
||||||
var tokenUsage = new ThorUsageResponse
|
var tokenUsage = new ThorUsageResponse
|
||||||
{
|
{
|
||||||
@@ -1041,8 +1041,7 @@ public class AiGateWayManager : DomainService
|
|||||||
}
|
}
|
||||||
|
|
||||||
//设置存储base64和url
|
//设置存储base64和url
|
||||||
imageStoreTask.StoreBase64 = imageBase64;
|
imageStoreTask.SetSuccess($"{ImageStoreHost}{storeUrl}");
|
||||||
imageStoreTask.SetSuccess(storeUrl);
|
|
||||||
await _imageStoreTaskRepository.UpdateAsync(imageStoreTask);
|
await _imageStoreTaskRepository.UpdateAsync(imageStoreTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ public class TokenValidationResult
|
|||||||
/// Token Id
|
/// Token Id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid TokenId { get; set; }
|
public Guid TokenId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// token
|
||||||
|
/// </summary>
|
||||||
|
public string Token { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TokenManager : DomainService
|
public class TokenManager : DomainService
|
||||||
@@ -43,25 +48,36 @@ public class TokenManager : DomainService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证Token并返回用户Id和TokenId
|
/// 验证Token并返回用户Id和TokenId
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="token">Token密钥</param>
|
/// <param name="tokenOrId">Token密钥或者TokenId</param>
|
||||||
/// <param name="modelId">模型Id(用于判断是否是尊享模型需要检查额度)</param>
|
/// <param name="modelId">模型Id(用于判断是否是尊享模型需要检查额度)</param>
|
||||||
/// <returns>Token验证结果</returns>
|
/// <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");
|
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();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var entity = await _tokenRepository._DbQueryable
|
{
|
||||||
.Where(x => x.Token == token)
|
var tokenStr = tokenOrId.ToString();
|
||||||
.FirstAsync();
|
if (!tokenStr.StartsWith("yi-"))
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("当前请求token非法", "401");
|
||||||
|
}
|
||||||
|
entity = await _tokenRepository._DbQueryable
|
||||||
|
.Where(x => x.Token == tokenStr)
|
||||||
|
.FirstAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (entity is null)
|
if (entity is null)
|
||||||
{
|
{
|
||||||
throw new UserFriendlyException("当前请求token无效", "401");
|
throw new UserFriendlyException("当前请求token无效", "401");
|
||||||
@@ -100,7 +116,8 @@ public class TokenManager : DomainService
|
|||||||
return new TokenValidationResult
|
return new TokenValidationResult
|
||||||
{
|
{
|
||||||
UserId = entity.UserId,
|
UserId = entity.UserId,
|
||||||
TokenId = entity.Id
|
TokenId = entity.Id,
|
||||||
|
Token = entity.Token
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\..\..\common.props" />
|
<Import Project="..\..\..\common.props" />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="OpenAI" Version="2.8.0" />
|
||||||
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
||||||
<PackageReference Include="Volo.Abp.Caching" Version="$(AbpVersion)" />
|
<PackageReference Include="Volo.Abp.Caching" Version="$(AbpVersion)" />
|
||||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.57.0" />
|
<PackageReference Include="Microsoft.SemanticKernel" Version="1.57.0" />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="OpenAI" Version="2.8.0" />
|
||||||
<PackageReference Include="Volo.Abp.AspNetCore.SignalR" Version="$(AbpVersion)" />
|
<PackageReference Include="Volo.Abp.AspNetCore.SignalR" Version="$(AbpVersion)" />
|
||||||
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
||||||
<PackageReference Include="Volo.Abp.Caching" 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())
|
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
// 每次触发只有2/24的概率执行生成新闻
|
// 每次触发只有2/24的概率执行生成新闻
|
||||||
var random = new Random();
|
var random = new Random();
|
||||||
var probability = random.Next(0, 24);
|
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())
|
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
await _stockMarketManager.GenerateStocksAsync();
|
await _stockMarketManager.GenerateStocksAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public class AutoPassInGoodsJob: HangfireBackgroundWorkerBase
|
|||||||
}
|
}
|
||||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
await _marketManager.AutoPassInGoodsAsync();
|
await _marketManager.AutoPassInGoodsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ public class AutoRefreshMiningPoolJob : HangfireBackgroundWorkerBase
|
|||||||
}
|
}
|
||||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
//刷新矿池
|
//刷新矿池
|
||||||
await _miningPoolManager.RefreshMiningPoolAsync();
|
await _miningPoolManager.RefreshMiningPoolAsync();
|
||||||
//刷新用户限制
|
//刷新用户限制
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class AutoUpdateCollectiblesValueJob : HangfireBackgroundWorkerBase
|
|||||||
|
|
||||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
await _collectiblesManager.UpdateAllValueAsync();
|
await _collectiblesManager.UpdateAllValueAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,7 @@ public class OnHookAutoMiningJob : HangfireBackgroundWorkerBase
|
|||||||
}
|
}
|
||||||
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
await _miningPoolManager.OnHookMiningAsync();
|
await _miningPoolManager.OnHookMiningAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ namespace Yi.Abp.Web
|
|||||||
//本地开发环境,可以禁用作业执行
|
//本地开发环境,可以禁用作业执行
|
||||||
if (host.IsDevelopment())
|
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
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
|
RoleClaimType = "Roles",
|
||||||
ClockSkew = TimeSpan.Zero,
|
ClockSkew = TimeSpan.Zero,
|
||||||
ValidateIssuerSigningKey = true,
|
ValidateIssuerSigningKey = true,
|
||||||
ValidIssuer = jwtOptions.Issuer,
|
ValidIssuer = jwtOptions.Issuer,
|
||||||
@@ -298,7 +299,8 @@ namespace Yi.Abp.Web
|
|||||||
}
|
}
|
||||||
else
|
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;
|
messageContext.Token = cookiesToken;
|
||||||
}
|
}
|
||||||
@@ -358,8 +360,8 @@ namespace Yi.Abp.Web
|
|||||||
var app = context.GetApplicationBuilder();
|
var app = context.GetApplicationBuilder();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
//app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<AiModelEntity>();
|
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ImageStoreTaskAggregateRoot>();
|
||||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeRecordAggregateRoot>();
|
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeRecordAggregateRoot>();
|
||||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<UsageStatisticsAggregateRoot>();
|
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<UsageStatisticsAggregateRoot>();
|
||||||
|
|
||||||
//跨域
|
//跨域
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<!-- 加载动画容器 -->
|
<!-- 加载动画容器 -->
|
||||||
<div id="yixinai-loader" class="loader-container">
|
<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-subtitle">海外地址,仅首次访问预计加载约10秒,无需梯子</div>
|
||||||
<div class="loader-logo">
|
<div class="loader-logo">
|
||||||
<div class="pulse-box"></div>
|
<div class="pulse-box"></div>
|
||||||
|
|||||||
@@ -4,5 +4,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</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;
|
||||||
|
}
|
||||||
@@ -6,3 +6,4 @@ export * from './model';
|
|||||||
export * from './pay';
|
export * from './pay';
|
||||||
export * from './session';
|
export * from './session';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
export * from './aiImage';
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ export function disableToken(id: string) {
|
|||||||
// 新增接口2
|
// 新增接口2
|
||||||
// 获取可选择的token信息
|
// 获取可选择的token信息
|
||||||
export function getSelectableTokenInfo() {
|
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 { CircleCheck, Loading } from '@element-plus/icons-vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { createOrder, getOrderStatus } from '@/api';
|
import { createOrder, getOrderStatus } from '@/api';
|
||||||
import { getGoodsList, GoodsCategoryType } from '@/api/pay';
|
import { getGoodsList, GoodsCategoryType } from '@/api/pay';
|
||||||
import SupportModelList from '@/components/userPersonalCenter/components/SupportModelList.vue';
|
import SupportModelList from '@/components/userPersonalCenter/components/SupportModelList.vue';
|
||||||
@@ -305,8 +304,6 @@ async function checkPaymentStatus(outTradeNo: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
function toggleDetails() {
|
function toggleDetails() {
|
||||||
showDetails.value = !showDetails.value;
|
showDetails.value = !showDetails.value;
|
||||||
}
|
}
|
||||||
@@ -322,7 +319,8 @@ function onClose() {
|
|||||||
|
|
||||||
function goToActivation() {
|
function goToActivation() {
|
||||||
close();
|
close();
|
||||||
userStore.openUserCenter('activationCode');
|
// 使用 window.location 进行跳转,避免 router 注入问题
|
||||||
|
window.location.href = '/console/activation';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -408,7 +408,10 @@ async function handleFlipCard(record: CardFlipRecord) {
|
|||||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
|
|
||||||
// 4. 移动克隆卡片到屏幕中心并放大(考虑边界限制)
|
// 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 scaledWidth = rect.width * scale;
|
||||||
const scaledHeight = rect.height * scale;
|
const scaledHeight = rect.height * scale;
|
||||||
|
|
||||||
@@ -416,8 +419,8 @@ async function handleFlipCard(record: CardFlipRecord) {
|
|||||||
let centerX = window.innerWidth / 2;
|
let centerX = window.innerWidth / 2;
|
||||||
let centerY = window.innerHeight / 2;
|
let centerY = window.innerHeight / 2;
|
||||||
|
|
||||||
// 边界检查:确保卡片完全在视口内(留20px边距)
|
// 边界检查:确保卡片完全在视口内(移动端留更多边距)
|
||||||
const margin = 20;
|
const margin = isMobile ? 30 : 20;
|
||||||
const minX = scaledWidth / 2 + margin;
|
const minX = scaledWidth / 2 + margin;
|
||||||
const maxX = window.innerWidth - scaledWidth / 2 - margin;
|
const maxX = window.innerWidth - scaledWidth / 2 - margin;
|
||||||
const minY = scaledHeight / 2 + margin;
|
const minY = scaledHeight / 2 + margin;
|
||||||
@@ -1253,6 +1256,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 自定义滚动条 */
|
/* 自定义滚动条 */
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@@ -1277,15 +1285,36 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
.lucky-float-ball {
|
.lucky-float-ball {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
/* left: 20px; */
|
transform: translateX(-50%);
|
||||||
/* top: 20px; */
|
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
transition: all 0.3s
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&: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 {
|
&.lucky-full {
|
||||||
@@ -1408,6 +1437,12 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
animation: slideIn 0.5s ease;
|
animation: slideIn 0.5s ease;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 8px 10px;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.compact-stats {
|
.compact-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1497,6 +1532,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shuffle-text {
|
.shuffle-text {
|
||||||
@@ -1505,6 +1545,15 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 2px 8px rgba(255, 215, 0, 0.6);
|
text-shadow: 0 2px 8px rgba(255, 215, 0, 0.6);
|
||||||
animation: textPulse 1.5s ease-in-out infinite;
|
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%;
|
max-width: 100%;
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 6px;
|
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);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-content {
|
.card-content {
|
||||||
@@ -1691,6 +1750,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 系统logo样式(优化为居中圆形,更丰富的效果)
|
// 系统logo样式(优化为居中圆形,更丰富的效果)
|
||||||
@@ -1709,6 +1772,12 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
z-index: 3;
|
z-index: 3;
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
// 外层光晕效果
|
// 外层光晕效果
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -1741,6 +1810,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-type-badge {
|
.card-type-badge {
|
||||||
@@ -1753,6 +1826,12 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
bottom: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-shine {
|
.card-shine {
|
||||||
@@ -1827,6 +1906,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
// logo水印样式
|
// logo水印样式
|
||||||
.result-watermark {
|
.result-watermark {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1869,6 +1952,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
filter: drop-shadow(0 4px 8px rgba(255, 215, 0, 0.5));
|
filter: drop-shadow(0 4px 8px rgba(255, 215, 0, 0.5));
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-text {
|
.result-text {
|
||||||
@@ -1883,6 +1970,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 16px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
// 文字外发光
|
// 文字外发光
|
||||||
&::after {
|
&::after {
|
||||||
content: attr(data-text);
|
content: attr(data-text);
|
||||||
@@ -1907,6 +1999,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
position: relative;
|
position: relative;
|
||||||
filter: drop-shadow(0 3px 10px rgba(255, 215, 0, 0.6));
|
filter: drop-shadow(0 3px 10px rgba(255, 215, 0, 0.6));
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 32px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
// 金色光效边框
|
// 金色光效边框
|
||||||
&::before {
|
&::before {
|
||||||
content: attr(data-amount);
|
content: attr(data-amount);
|
||||||
@@ -1932,6 +2029,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
letter-spacing: 3px;
|
letter-spacing: 3px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-top: 4px;
|
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;
|
margin-bottom: 6px;
|
||||||
filter: drop-shadow(0 2px 6px rgba(147, 112, 219, 0.3));
|
filter: drop-shadow(0 2px 6px rgba(147, 112, 219, 0.3));
|
||||||
animation: gentleBounce 2s ease-in-out infinite;
|
animation: gentleBounce 2s ease-in-out infinite;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-text {
|
.result-text {
|
||||||
@@ -1964,6 +2070,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-tip {
|
.result-tip {
|
||||||
@@ -1972,6 +2082,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1994,6 +2108,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
filter: drop-shadow(0 4px 12px rgba(255, 215, 0, 0.8));
|
filter: drop-shadow(0 4px 12px rgba(255, 215, 0, 0.8));
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 42px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mystery-text {
|
.mystery-text {
|
||||||
@@ -2004,6 +2123,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
letter-spacing: 4px;
|
letter-spacing: 4px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mystery-hint {
|
.mystery-hint {
|
||||||
@@ -2012,6 +2136,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mystery-stars {
|
.mystery-stars {
|
||||||
@@ -2060,6 +2189,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-input {
|
.code-input {
|
||||||
@@ -2078,10 +2212,19 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
border: 1px solid #bae6fd;
|
border: 1px solid #bae6fd;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 20px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.filled-icon {
|
.filled-icon {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 40px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filled-text {
|
.filled-text {
|
||||||
@@ -2090,6 +2233,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2128,12 +2275,22 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.code-text {
|
.code-text {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
letter-spacing: 6px;
|
letter-spacing: 6px;
|
||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
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;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.el-button {
|
.el-button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2153,6 +2319,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -2199,12 +2369,20 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.share-preview-title {
|
.share-preview-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-preview-content {
|
.share-preview-content {
|
||||||
@@ -2218,6 +2396,12 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
max-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 自定义滚动条 */
|
/* 自定义滚动条 */
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
@@ -2276,9 +2460,17 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.double-icon {
|
.double-icon {
|
||||||
font-size: 64px;
|
font-size: 64px;
|
||||||
animation: bounce 1s infinite;
|
animation: bounce 1s infinite;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 52px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.double-sparkle {
|
.double-sparkle {
|
||||||
@@ -2287,6 +2479,10 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
right: -10px;
|
right: -10px;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
animation: spin 2s linear infinite;
|
animation: spin 2s linear infinite;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2295,6 +2491,11 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.double-text {
|
.double-text {
|
||||||
@@ -2303,10 +2504,19 @@ function getCardClass(record: CardFlipRecord): string[] {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import type { DailyTaskItem, DailyTaskStatusOutput } from '@/api/dailyTask/types';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { getTodayTaskStatus, claimTaskReward } from '@/api/dailyTask';
|
import { onMounted, ref } from 'vue';
|
||||||
import type { DailyTaskStatusOutput, DailyTaskItem } from '@/api/dailyTask/types';
|
import { claimTaskReward, getTodayTaskStatus } from '@/api/dailyTask';
|
||||||
|
|
||||||
const taskData = ref<DailyTaskStatusOutput | null>(null);
|
const taskData = ref<DailyTaskStatusOutput | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -17,15 +17,18 @@ async function fetchTaskStatus() {
|
|||||||
try {
|
try {
|
||||||
const res = await getTodayTaskStatus();
|
const res = await getTodayTaskStatus();
|
||||||
taskData.value = res.data;
|
taskData.value = res.data;
|
||||||
} catch (error: any) {
|
}
|
||||||
|
catch (error: any) {
|
||||||
ElMessage.error(error?.message || '获取任务状态失败');
|
ElMessage.error(error?.message || '获取任务状态失败');
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClaim(task: DailyTaskItem) {
|
async function handleClaim(task: DailyTaskItem) {
|
||||||
if (task.status !== 1) return;
|
if (task.status !== 1)
|
||||||
|
return;
|
||||||
|
|
||||||
claiming.value[task.level] = true;
|
claiming.value[task.level] = true;
|
||||||
try {
|
try {
|
||||||
@@ -34,9 +37,11 @@ async function handleClaim(task: DailyTaskItem) {
|
|||||||
|
|
||||||
// 刷新任务状态
|
// 刷新任务状态
|
||||||
await fetchTaskStatus();
|
await fetchTaskStatus();
|
||||||
} catch (error: any) {
|
}
|
||||||
|
catch (error: any) {
|
||||||
ElMessage.error(error?.message || '领取奖励失败');
|
ElMessage.error(error?.message || '领取奖励失败');
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
claiming.value[task.level] = false;
|
claiming.value[task.level] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,8 +81,10 @@ function getButtonClass(task: DailyTaskItem): string {
|
|||||||
|
|
||||||
// 获取进度条颜色
|
// 获取进度条颜色
|
||||||
function getProgressColor(task: DailyTaskItem): string {
|
function getProgressColor(task: DailyTaskItem): string {
|
||||||
if (task.status === 2) return '#FFD700'; // 已完成:金色
|
if (task.status === 2)
|
||||||
if (task.status === 1) return '#67C23A'; // 可领取:绿色
|
return '#FFD700'; // 已完成:金色
|
||||||
|
if (task.status === 1)
|
||||||
|
return '#67C23A'; // 可领取:绿色
|
||||||
return '#409EFF'; // 进行中:蓝色
|
return '#409EFF'; // 进行中:蓝色
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -86,15 +93,21 @@ function getProgressColor(task: DailyTaskItem): string {
|
|||||||
<div v-loading="loading" class="daily-task-container">
|
<div v-loading="loading" class="daily-task-container">
|
||||||
<div class="task-header">
|
<div class="task-header">
|
||||||
<h2>每日任务</h2>
|
<h2>每日任务</h2>
|
||||||
<p class="task-desc">完成每日任务,领取额外尊享包 Token 奖励,可累加重复</p>
|
<p class="task-desc">
|
||||||
|
完成每日任务,领取额外尊享包 Token 奖励,可累加重复
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="taskData" class="task-content">
|
<div v-if="taskData" class="task-content">
|
||||||
<!-- 今日消耗统计 -->
|
<!-- 今日消耗统计 -->
|
||||||
<div class="consumption-card">
|
<div class="consumption-card">
|
||||||
<div class="consumption-icon">🔥</div>
|
<div class="consumption-icon">
|
||||||
|
🔥
|
||||||
|
</div>
|
||||||
<div class="consumption-info">
|
<div class="consumption-info">
|
||||||
<div class="consumption-label">今日尊享包消耗</div>
|
<div class="consumption-label">
|
||||||
|
今日尊享包消耗
|
||||||
|
</div>
|
||||||
<div class="consumption-value">
|
<div class="consumption-value">
|
||||||
{{ formatTokenDisplay(taskData.todayConsumedTokens) }} Tokens
|
{{ formatTokenDisplay(taskData.todayConsumedTokens) }} Tokens
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +122,7 @@ function getProgressColor(task: DailyTaskItem): string {
|
|||||||
class="task-item"
|
class="task-item"
|
||||||
:class="{
|
:class="{
|
||||||
'task-completed': task.status === 2,
|
'task-completed': task.status === 2,
|
||||||
'task-claimable': task.status === 1
|
'task-claimable': task.status === 1,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="task-icon">
|
<div class="task-icon">
|
||||||
@@ -187,7 +200,6 @@ function getProgressColor(task: DailyTaskItem): string {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.daily-task-container {
|
.daily-task-container {
|
||||||
padding: 20px;
|
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -83,10 +83,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.premium-service {
|
.premium-service {
|
||||||
padding: 24px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%);
|
//background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
|
||||||
// 美化滚动条
|
// 美化滚动条
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
@@ -127,7 +126,6 @@ onMounted(() => {
|
|||||||
/* 响应式布局 */
|
/* 响应式布局 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.premium-service {
|
.premium-service {
|
||||||
padding: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-list-wrapper {
|
.usage-list-wrapper {
|
||||||
|
|||||||
@@ -599,18 +599,12 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.usage-statistics {
|
.usage-statistics {
|
||||||
padding: 20px;
|
|
||||||
position: relative;
|
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;
|
transition: all 0.3s ease;
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-statistics:hover {
|
.usage-statistics:hover {
|
||||||
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-statistics.fullscreen-mode {
|
.usage-statistics.fullscreen-mode {
|
||||||
@@ -620,7 +614,6 @@ onBeforeUnmount(() => {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -632,7 +625,6 @@ onBeforeUnmount(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
border-bottom: 2px solid #e9ecef;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
@@ -656,7 +648,6 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.option-label {
|
.option-label {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
color: #909399;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -671,7 +662,6 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.disabled-icon {
|
&.disabled-icon {
|
||||||
color: #c0c4cc;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,14 +700,10 @@ onBeforeUnmount(() => {
|
|||||||
.chart-card {
|
.chart-card {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-card:hover {
|
.chart-card:hover {
|
||||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
|
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -303,7 +303,6 @@ function bindWechat() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.user-profile {
|
.user-profile {
|
||||||
padding: 20px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ export const COLLAPSE_THRESHOLD: number = 600;
|
|||||||
export const SIDE_BAR_WIDTH: number = 280;
|
export const SIDE_BAR_WIDTH: number = 280;
|
||||||
|
|
||||||
// 路由白名单地址[本地存在的路由 staticRouter.ts 中]
|
// 路由白名单地址[本地存在的路由 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import Header from '@/layouts/components/Header/index.vue';
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--color-gray-100);
|
background: var(--color-white);
|
||||||
.layout-header {
|
.layout-header {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-bottom: var(--header-border) ;
|
border-bottom: var(--header-border) ;
|
||||||
|
|||||||
@@ -153,7 +153,6 @@ function toggleSidebar() {
|
|||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="header-content-collapsed flex items-center justify-center hover:cursor-pointer"
|
class="header-content-collapsed flex items-center justify-center hover:cursor-pointer"
|
||||||
@click="handleLogoClick"
|
|
||||||
>
|
>
|
||||||
<el-icon size="20">
|
<el-icon size="20">
|
||||||
<ChatLineSquare />
|
<ChatLineSquare />
|
||||||
@@ -188,7 +187,7 @@ function toggleSidebar() {
|
|||||||
class="creat-chat-btn"
|
class="creat-chat-btn"
|
||||||
:class="{
|
:class="{
|
||||||
'creat-chat-btn-collapsed': isCollapsed,
|
'creat-chat-btn-collapsed': isCollapsed,
|
||||||
'is-disabled': isNewChatState
|
'is-disabled': isNewChatState,
|
||||||
}"
|
}"
|
||||||
@click="!isNewChatState && handleCreatChat()"
|
@click="!isNewChatState && handleCreatChat()"
|
||||||
>
|
>
|
||||||
@@ -306,7 +305,7 @@ function toggleSidebar() {
|
|||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background-color: var(--sidebar-background-color, #f9fafb);
|
//background-color: var(--sidebar-background-color, #f9fafb);
|
||||||
|
|
||||||
// 展开状态 - 240px
|
// 展开状态 - 240px
|
||||||
&:not(.aside-collapsed) {
|
&:not(.aside-collapsed) {
|
||||||
@@ -401,7 +400,7 @@ function toggleSidebar() {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 4px;
|
//padding: 0 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.creat-chat-btn-wrapper {
|
.creat-chat-btn-wrapper {
|
||||||
@@ -760,6 +759,7 @@ function toggleSidebar() {
|
|||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
&.aside-collapsed {
|
&.aside-collapsed {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
|
|||||||
@@ -14,27 +14,6 @@ function openTutorial() {
|
|||||||
>
|
>
|
||||||
<!-- PC端显示文字 -->
|
<!-- PC端显示文字 -->
|
||||||
<span class="pc-text">文档</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useAnnouncementStore } from '@/stores';
|
import { useAnnouncementStore } from '@/stores';
|
||||||
|
|
||||||
const announcementStore = useAnnouncementStore();
|
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() {
|
function openAnnouncement() {
|
||||||
announcementStore.openDialog();
|
announcementStore.openDialog();
|
||||||
}
|
}
|
||||||
@@ -20,93 +10,127 @@ function openAnnouncement() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="announcement-btn-container" data-tour="announcement-btn">
|
<div class="announcement-btn-container" data-tour="announcement-btn">
|
||||||
<el-badge
|
<div
|
||||||
is-dot
|
class="announcement-btn"
|
||||||
class="announcement-badge"
|
title="查看公告"
|
||||||
|
@click="openAnnouncement"
|
||||||
>
|
>
|
||||||
<!-- :value="unreadCount" -->
|
<!-- PC端显示文字 -->
|
||||||
<!-- :hidden="unreadCount === 0" -->
|
<span class="pc-text">公告</span>
|
||||||
<!-- :max="99" -->
|
</div>
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped>
|
||||||
.announcement-btn-container {
|
.announcement-btn-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.announcement-badge {
|
.announcement-btn {
|
||||||
:deep(.el-badge__content) {
|
display: flex;
|
||||||
background-color: #f56c6c;
|
align-items: center;
|
||||||
border: none;
|
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;
|
||||||
}
|
}
|
||||||
|
50% {
|
||||||
.announcement-btn {
|
transform: scale(1.1);
|
||||||
display: flex;
|
opacity: 0.4;
|
||||||
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) {
|
.mobile-icon {
|
||||||
.announcement-btn-container {
|
display: none;
|
||||||
.announcement-btn {
|
position: relative;
|
||||||
.pc-text {
|
width: 20px;
|
||||||
display: none;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
<!-- 头像 -->
|
<!-- 头像 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChatLineRound } from '@element-plus/icons-vue';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import Popover from '@/components/Popover/index.vue';
|
import Popover from '@/components/Popover/index.vue';
|
||||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||||
import { useAnnouncementStore, useGuideTourStore, useUserStore } from '@/stores';
|
import { useUserStore } from '@/stores';
|
||||||
import { useSessionStore } from '@/stores/modules/session';
|
import { useSessionStore } from '@/stores/modules/session';
|
||||||
import { getUserProfilePicture, isUserVip } from '@/utils/user';
|
import { getUserProfilePicture, isUserVip } from '@/utils/user';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
const guideTourStore = useGuideTourStore();
|
const { startHeaderTour } = useGuideTour();
|
||||||
const announcementStore = useAnnouncementStore();
|
|
||||||
const { startUserCenterTour } = useGuideTour();
|
|
||||||
|
|
||||||
/* 弹出面板 开始 */
|
/* 弹出面板 开始 */
|
||||||
const popoverStyle = ref({
|
const popoverStyle = ref({
|
||||||
@@ -29,41 +25,17 @@ const popoverRef = ref();
|
|||||||
|
|
||||||
// 弹出面板内容
|
// 弹出面板内容
|
||||||
const popoverList = ref([
|
const popoverList = ref([
|
||||||
|
{
|
||||||
// {
|
key: '1',
|
||||||
// key: '5',
|
title: '用户信息',
|
||||||
// title: '控制台',
|
icon: 'settings-4-fill',
|
||||||
// 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',
|
// key: '6',
|
||||||
// title: '新手引导',
|
// title: '新手引导',
|
||||||
// icon: 'dashboard-fill',
|
// icon: 'dashboard-fill',
|
||||||
// },
|
// },
|
||||||
// {
|
|
||||||
// key: '3',
|
|
||||||
// divider: true,
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
key: '4',
|
key: '4',
|
||||||
title: '退出登录',
|
title: '退出登录',
|
||||||
@@ -71,87 +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() {
|
function handleStartTutorial() {
|
||||||
startHeaderTour();
|
startHeaderTour();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击
|
// 点击
|
||||||
function handleClick(item: any) {
|
function handleClick(item: any) {
|
||||||
switch (item.key) {
|
switch (item.key) {
|
||||||
case '1':
|
case '1':
|
||||||
ElMessage.warning('暂未开放');
|
router.push('/console/user');
|
||||||
break;
|
|
||||||
case '2':
|
|
||||||
ElMessage.warning('暂未开放');
|
|
||||||
break;
|
|
||||||
case '5':
|
|
||||||
// 打开控制台
|
|
||||||
popoverRef.value?.hide?.();
|
|
||||||
router.push('/console');
|
|
||||||
break;
|
break;
|
||||||
case '6':
|
case '6':
|
||||||
handleStartTutorial();
|
handleStartTutorial();
|
||||||
break;
|
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':
|
case '4':
|
||||||
popoverRef.value?.hide?.();
|
popoverRef.value?.hide?.();
|
||||||
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
|
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
|
||||||
@@ -175,10 +80,7 @@ function handleClick(item: any) {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// ElMessage({
|
// 取消退出,不执行任何操作
|
||||||
// type: 'info',
|
|
||||||
// message: '取消',
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -222,124 +124,15 @@ function openVipGuide() {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div class="text-sm font-semibold text-gray-800">
|
||||||
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
|
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
|
||||||
</div>
|
</div>
|
||||||
@@ -356,6 +149,7 @@ defineExpose({
|
|||||||
<span
|
<span
|
||||||
v-else
|
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"
|
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>
|
</span>
|
||||||
@@ -363,7 +157,7 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 头像区域 -->
|
<!-- 头像区域 -->
|
||||||
<div class="avatar-container" data-tour="user-avatar">
|
<div class="avatar-container">
|
||||||
<Popover
|
<Popover
|
||||||
ref="popoverRef"
|
ref="popoverRef"
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@@ -395,6 +189,7 @@ defineExpose({
|
|||||||
<span
|
<span
|
||||||
v-else
|
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"
|
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>
|
</span>
|
||||||
@@ -405,7 +200,6 @@ defineExpose({
|
|||||||
|
|
||||||
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
|
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
|
||||||
<div
|
<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)]"
|
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)"
|
@click="handleClick(item)"
|
||||||
>
|
>
|
||||||
@@ -414,78 +208,15 @@ defineExpose({
|
|||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="item.divider" class="divder h-1px bg-gray-200 my-4px" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.popover-content {
|
.popover-content{
|
||||||
width: 520px;
|
width: 520px;
|
||||||
height: 520px;
|
height: 520px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,133 +1,5 @@
|
|||||||
<!--
|
|
||||||
<!– Header 头部 –>
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router';
|
import { computed, ref } from 'vue';
|
||||||
import logo from '@/assets/images/logo.png';
|
|
||||||
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 ConsoleBtn from './components/ConsoleBtn.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';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
// 打开控制台
|
|
||||||
function handleOpenConsole() {
|
|
||||||
router.push('/console');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="header-container">
|
|
||||||
<div class="header-box">
|
|
||||||
<!– 左侧logo和品牌区域 –>
|
|
||||||
<div class="left-section">
|
|
||||||
<div class="brand-container">
|
|
||||||
<el-image :src="logo" alt="logo" fit="contain" class="logo-img" />
|
|
||||||
<span class="brand-text">意心AI</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!– 右侧功能按钮区域 –>
|
|
||||||
<div class="right-section">
|
|
||||||
<StartChatBtn />
|
|
||||||
<AnnouncementBtn />
|
|
||||||
<ModelLibraryBtn />
|
|
||||||
<AiTutorialBtn />
|
|
||||||
<ConsoleBtn @open-console="handleOpenConsole" />
|
|
||||||
<BuyBtn v-show="userStore.userInfo" />
|
|
||||||
<ThemeBtn />
|
|
||||||
<LoginBtn v-show="!userStore.userInfo" />
|
|
||||||
<Avatar v-show="userStore.userInfo" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.header-container {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: var(--header-container-default-height, 60px);
|
|
||||||
|
|
||||||
.header-box {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 16px;
|
|
||||||
background: var(--header-bg-color, #ffffff);
|
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 左侧品牌区域
|
|
||||||
.left-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-width: fit-content;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.brand-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.logo-img {
|
|
||||||
width: 36px; // 优化为更合适的大小
|
|
||||||
height: 36px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-text {
|
|
||||||
font-size: 22px; // 减小字体大小
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--brand-color, #000000);
|
|
||||||
white-space: nowrap;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
//color: var(--brand-hover-color, #40a9ff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 右侧功能区域
|
|
||||||
.right-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px; // 优化按钮间距
|
|
||||||
height: 100%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
// 统一按钮样式
|
|
||||||
:deep(.menu-button) {
|
|
||||||
height: 36px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
-->
|
|
||||||
<!-- Header 头部 -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import logo from '@/assets/images/logo.png';
|
import logo from '@/assets/images/logo.png';
|
||||||
import ConsoleBtn from '@/layouts/components0/Header/components/ConsoleBtn.vue';
|
import ConsoleBtn from '@/layouts/components0/Header/components/ConsoleBtn.vue';
|
||||||
@@ -144,6 +16,9 @@ const router = useRouter();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 移动端菜单抽屉状态
|
||||||
|
const mobileMenuVisible = ref(false);
|
||||||
|
|
||||||
// 当前激活的菜单项
|
// 当前激活的菜单项
|
||||||
const activeIndex = computed(() => {
|
const activeIndex = computed(() => {
|
||||||
if (route.path.startsWith('/console'))
|
if (route.path.startsWith('/console'))
|
||||||
@@ -159,15 +34,36 @@ const activeIndex = computed(() => {
|
|||||||
function handleSelect(key: string) {
|
function handleSelect(key: string) {
|
||||||
if (key && key !== 'no-route') {
|
if (key && key !== 'no-route') {
|
||||||
router.push(key);
|
router.push(key);
|
||||||
|
mobileMenuVisible.value = false; // 移动端导航后关闭菜单
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修改 AI 聊天菜单的点击事件
|
||||||
|
function handleAIClick(e: MouseEvent) {
|
||||||
|
e.stopPropagation(); // 阻止事件冒泡
|
||||||
|
router.push('/chat/conversation');
|
||||||
|
mobileMenuVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改控制台菜单的点击事件
|
||||||
|
function handleConsoleClick(e: MouseEvent) {
|
||||||
|
e.stopPropagation(); // 阻止事件冒泡
|
||||||
|
router.push('/console/user');
|
||||||
|
mobileMenuVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换移动端菜单
|
||||||
|
function toggleMobileMenu() {
|
||||||
|
mobileMenuVisible.value = !mobileMenuVisible.value;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
|
<!-- 桌面端菜单 -->
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeIndex"
|
:default-active="activeIndex"
|
||||||
class="header-menu"
|
class="header-menu desktop-menu"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
:ellipsis="false"
|
:ellipsis="false"
|
||||||
:router="false"
|
:router="false"
|
||||||
@@ -186,16 +82,19 @@ function handleSelect(key: string) {
|
|||||||
<!-- AI聊天菜单 -->
|
<!-- AI聊天菜单 -->
|
||||||
<el-sub-menu index="chat" class="chat-submenu" popper-class="custom-popover">
|
<el-sub-menu index="chat" class="chat-submenu" popper-class="custom-popover">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="menu-title">AI聊天</span>
|
<span class="menu-title" @click="handleAIClick">AI应用</span>
|
||||||
</template>
|
</template>
|
||||||
<el-menu-item index="/chat/conversation">
|
<el-menu-item index="/chat/conversation">
|
||||||
AI对话
|
AI对话
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/chat/image">
|
<el-menu-item index="/chat/image">
|
||||||
图片生成
|
AI图片
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/chat/video">
|
<el-menu-item index="/chat/video">
|
||||||
视频生成
|
AI视频
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/chat/agent">
|
||||||
|
AI智能体
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
|
|
||||||
@@ -217,7 +116,7 @@ function handleSelect(key: string) {
|
|||||||
<!-- 控制台菜单 -->
|
<!-- 控制台菜单 -->
|
||||||
<el-sub-menu index="console" class="console-submenu" popper-class="custom-popover">
|
<el-sub-menu index="console" class="console-submenu" popper-class="custom-popover">
|
||||||
<template #title>
|
<template #title>
|
||||||
<ConsoleBtn />
|
<ConsoleBtn @click="handleConsoleClick" />
|
||||||
</template>
|
</template>
|
||||||
<el-menu-item index="/console/user">
|
<el-menu-item index="/console/user">
|
||||||
用户信息
|
用户信息
|
||||||
@@ -266,12 +165,103 @@ function handleSelect(key: string) {
|
|||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</div>
|
</div>
|
||||||
</el-menu>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.header-container {
|
.header-container {
|
||||||
--menu-hover-bg: #f5f5f5;
|
--menu-hover-bg: var(--color-white);
|
||||||
--menu-active-color: var(--el-color-primary);
|
--menu-active-color: var(--el-color-primary);
|
||||||
--menu-transition: all 0.2s ease;
|
--menu-transition: all 0.2s ease;
|
||||||
|
|
||||||
@@ -279,7 +269,12 @@ function handleSelect(key: string) {
|
|||||||
height: var(--header-container-default-height, 64px);
|
height: var(--header-container-default-height, 64px);
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端头部(默认隐藏)
|
||||||
|
.mobile-header {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-menu {
|
.header-menu {
|
||||||
@@ -289,6 +284,8 @@ function handleSelect(key: string) {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
|
//background: var(--color-white);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 左侧品牌区域
|
// 左侧品牌区域
|
||||||
@@ -344,7 +341,7 @@ function handleSelect(key: string) {
|
|||||||
:deep(.el-sub-menu__title) {
|
:deep(.el-sub-menu__title) {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
padding: 0 12px !important;
|
padding: 0 4px !important;
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -401,10 +398,116 @@ function handleSelect(key: string) {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 12px;
|
padding: 0 4px;
|
||||||
margin-left: 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) {
|
@media (max-width: 1280px) {
|
||||||
.brand-text {
|
.brand-text {
|
||||||
@@ -451,52 +554,37 @@ function handleSelect(key: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.brand-text {
|
// 隐藏桌面端菜单
|
||||||
display: none;
|
.desktop-menu {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-img {
|
// 显示移动端头部
|
||||||
width: 32px;
|
.mobile-header {
|
||||||
height: 32px;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
.menu-left {
|
height: 100%;
|
||||||
margin-left: 12px;
|
padding: 0 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.menu-right {
|
|
||||||
margin-right: 8px;
|
|
||||||
|
|
||||||
// 隐藏按钮文字
|
|
||||||
:deep(.button-text) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-title {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示图标
|
|
||||||
:deep(.el-icon) {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item),
|
|
||||||
:deep(.el-sub-menu__title) {
|
|
||||||
padding: 0 8px !important;
|
|
||||||
min-width: auto !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.menu-right {
|
.mobile-header {
|
||||||
gap: 0;
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-menu-item),
|
.mobile-brand-text {
|
||||||
:deep(.el-sub-menu__title) {
|
font-size: 16px;
|
||||||
padding: 0 6px !important;
|
}
|
||||||
}
|
|
||||||
|
.mobile-logo {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-actions {
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ function handleContextMenu(event: MouseEvent, item: ConversationItem<ChatSession
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 4px;
|
//padding: 0 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.creat-chat-btn-wrapper {
|
.creat-chat-btn-wrapper {
|
||||||
|
|||||||
@@ -67,4 +67,23 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
||||||
618
Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue
Normal file
618
Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { UploadFile, UploadUserFile } from 'element-plus';
|
||||||
|
import type { ImageModel, TaskStatusResponse } from '@/api/aiImage/types';
|
||||||
|
import {
|
||||||
|
CircleCloseFilled,
|
||||||
|
CopyDocument,
|
||||||
|
Delete,
|
||||||
|
Download,
|
||||||
|
MagicStick,
|
||||||
|
Picture as PictureIcon,
|
||||||
|
Plus,
|
||||||
|
Refresh,
|
||||||
|
ZoomIn,
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import { useClipboard } from '@vueuse/core';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
import { getSelectableTokenInfo } from '@/api';
|
||||||
|
import { generateImage, getImageModels, getTaskStatus } from '@/api/aiImage';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['task-created']);
|
||||||
|
|
||||||
|
const { copy } = useClipboard();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const tokenOptions = ref<any[]>([]);
|
||||||
|
const selectedTokenId = ref('');
|
||||||
|
const tokenLoading = ref(false);
|
||||||
|
|
||||||
|
const modelOptions = ref<ImageModel[]>([]);
|
||||||
|
const selectedModelId = ref('');
|
||||||
|
const modelLoading = ref(false);
|
||||||
|
|
||||||
|
const prompt = ref('');
|
||||||
|
const fileList = ref<UploadUserFile[]>([]);
|
||||||
|
const compressImage = ref(true);
|
||||||
|
|
||||||
|
const generating = ref(false);
|
||||||
|
const currentTaskId = ref('');
|
||||||
|
const currentTask = ref<TaskStatusResponse | null>(null);
|
||||||
|
const showViewer = ref(false);
|
||||||
|
let pollTimer: any = null;
|
||||||
|
|
||||||
|
const canGenerate = computed(() => {
|
||||||
|
return selectedModelId.value && prompt.value && !generating.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch isActive to manage polling
|
||||||
|
watch(() => props.isActive, (active) => {
|
||||||
|
if (active) {
|
||||||
|
// Resume polling if we have a processing task
|
||||||
|
if (currentTaskId.value && currentTask.value?.taskStatus === 'Processing') {
|
||||||
|
startPolling(currentTaskId.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stopPolling();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
async function fetchTokens() {
|
||||||
|
tokenLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getSelectableTokenInfo();
|
||||||
|
// Handle potential wrapper
|
||||||
|
const data = Array.isArray(res) ? res : (res as any).data || [];
|
||||||
|
|
||||||
|
// Add Default Option
|
||||||
|
tokenOptions.value = [
|
||||||
|
{ tokenId: '', name: '默认 (Default)', isDisabled: false },
|
||||||
|
...data,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Default select "Default" if available, otherwise first available
|
||||||
|
if (!selectedTokenId.value) {
|
||||||
|
selectedTokenId.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
tokenLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchModels() {
|
||||||
|
modelLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getImageModels();
|
||||||
|
// Handle potential wrapper
|
||||||
|
const data = Array.isArray(res) ? res : (res as any).data || [];
|
||||||
|
modelOptions.value = data;
|
||||||
|
|
||||||
|
// Default select first
|
||||||
|
if (modelOptions.value.length > 0 && !selectedModelId.value) {
|
||||||
|
selectedModelId.value = modelOptions.value[0].modelId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
modelLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileChange(uploadFile: UploadFile) {
|
||||||
|
const isLt5M = uploadFile.size! / 1024 / 1024 < 5;
|
||||||
|
if (!isLt5M) {
|
||||||
|
ElMessage.error('图片大小不能超过 5MB!');
|
||||||
|
const index = fileList.value.indexOf(uploadFile);
|
||||||
|
if (index !== -1)
|
||||||
|
fileList.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemove(file: UploadFile) {
|
||||||
|
const index = fileList.value.indexOf(file);
|
||||||
|
if (index !== -1)
|
||||||
|
fileList.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileToBase64(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.onerror = error => reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGenerate() {
|
||||||
|
if (!canGenerate.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
generating.value = true;
|
||||||
|
// Reset current task display immediately
|
||||||
|
currentTask.value = {
|
||||||
|
id: '',
|
||||||
|
prompt: prompt.value,
|
||||||
|
taskStatus: 'Processing',
|
||||||
|
publishStatus: 'Unpublished',
|
||||||
|
categories: [],
|
||||||
|
creationTime: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base64Images: string[] = [];
|
||||||
|
for (const fileItem of fileList.value) {
|
||||||
|
if (fileItem.raw) {
|
||||||
|
const base64 = await fileToBase64(fileItem.raw);
|
||||||
|
base64Images.push(base64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await generateImage({
|
||||||
|
tokenId: selectedTokenId.value || undefined, // Send undefined if empty
|
||||||
|
prompt: prompt.value,
|
||||||
|
modelId: selectedModelId.value,
|
||||||
|
referenceImagesPrefixBase64: base64Images,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Robust ID extraction: Handle string, object wrapper, or direct object
|
||||||
|
let taskId = '';
|
||||||
|
if (typeof res === 'string') {
|
||||||
|
taskId = res;
|
||||||
|
}
|
||||||
|
else if (typeof res === 'object' && res !== null) {
|
||||||
|
// Check for data property which might contain the ID string or object
|
||||||
|
const data = (res as any).data;
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
taskId = data;
|
||||||
|
}
|
||||||
|
else if (typeof data === 'object' && data !== null) {
|
||||||
|
taskId = data.id || data.taskId;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Fallback to direct properties
|
||||||
|
taskId = (res as any).id || (res as any).taskId || (res as any).result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!taskId) {
|
||||||
|
console.error('Task ID not found in response:', res);
|
||||||
|
throw new Error('Invalid Task ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTaskId.value = taskId;
|
||||||
|
startPolling(taskId);
|
||||||
|
emit('task-created');
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
ElMessage.error('生成任务创建失败');
|
||||||
|
currentTask.value = null;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
generating.value = false; // Allow new tasks immediately
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling(taskId: string) {
|
||||||
|
if (pollTimer)
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
pollStatus(taskId);
|
||||||
|
|
||||||
|
pollTimer = setInterval(() => {
|
||||||
|
pollStatus(taskId);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollStatus(taskId: string) {
|
||||||
|
// Double check active status before polling (though timer should be cleared)
|
||||||
|
if (!props.isActive) {
|
||||||
|
stopPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getTaskStatus(taskId);
|
||||||
|
// Handle response structure if needed
|
||||||
|
const taskData = (res as any).data || res;
|
||||||
|
|
||||||
|
// Only update if it matches the current task we are watching
|
||||||
|
// This prevents race conditions if user starts a new task
|
||||||
|
if (currentTaskId.value === taskId) {
|
||||||
|
currentTask.value = taskData;
|
||||||
|
|
||||||
|
// Case-insensitive check just in case
|
||||||
|
const status = taskData.taskStatus;
|
||||||
|
|
||||||
|
if (status === 'Success' || status === 'Fail') {
|
||||||
|
stopPolling();
|
||||||
|
if (status === 'Success') {
|
||||||
|
ElMessage.success('图片生成成功');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ElMessage.error(taskData.errorInfo || '图片生成失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If task ID changed, stop polling this old task
|
||||||
|
// Actually, we should probably just let the new task's poller handle it
|
||||||
|
// But since we use a single pollTimer variable, starting a new task clears the old timer.
|
||||||
|
// So this check is mostly for the initial async call returning after new task started.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
// Don't stop polling on transient network errors, but maybe log it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
pollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPrompt() {
|
||||||
|
prompt.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadImage() {
|
||||||
|
if (currentTask.value?.storeUrl) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(currentTask.value.storeUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `generated-image-${currentTask.value.id}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Download failed', e);
|
||||||
|
// Fallback
|
||||||
|
window.open(currentTask.value.storeUrl, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyError(text: string) {
|
||||||
|
await copy(text);
|
||||||
|
ElMessage.success('错误信息已复制');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exposed methods for external control
|
||||||
|
function setPrompt(text: string) {
|
||||||
|
prompt.value = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to load image from URL and convert to File object
|
||||||
|
async function addReferenceImage(url: string) {
|
||||||
|
if (fileList.value.length >= 2) {
|
||||||
|
ElMessage.warning('最多只能上传2张参考图');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const filename = url.split('/').pop() || 'reference.png';
|
||||||
|
const file = new File([blob], filename, { type: blob.type });
|
||||||
|
|
||||||
|
const uploadFile: UploadUserFile = {
|
||||||
|
name: filename,
|
||||||
|
url,
|
||||||
|
raw: file,
|
||||||
|
uid: Date.now(),
|
||||||
|
status: 'ready',
|
||||||
|
};
|
||||||
|
|
||||||
|
fileList.value.push(uploadFile);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Failed to load reference image', e);
|
||||||
|
ElMessage.error('无法加载参考图');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
setPrompt,
|
||||||
|
addReferenceImage,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTokens();
|
||||||
|
fetchModels();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full md:flex-row gap-4 md:gap-6 p-3 md:p-4 bg-white rounded-lg shadow-sm">
|
||||||
|
<!-- Left Config Panel -->
|
||||||
|
<div class="w-full md:w-[400px] flex flex-col gap-4 md:gap-6 overflow-y-auto pr-0 md:pr-2 custom-scrollbar">
|
||||||
|
<div class="space-y-3 md:space-y-4">
|
||||||
|
<h2 class="text-base md:text-lg font-bold text-gray-800 flex items-center gap-2">
|
||||||
|
<el-icon><MagicStick /></el-icon>
|
||||||
|
配置
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<el-form label-position="top" class="space-y-2">
|
||||||
|
<!-- Token -->
|
||||||
|
<el-form-item label="API密钥 (可选)">
|
||||||
|
<el-select
|
||||||
|
v-model="selectedTokenId"
|
||||||
|
placeholder="请选择API密钥"
|
||||||
|
class="w-full"
|
||||||
|
:loading="tokenLoading"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="token in tokenOptions"
|
||||||
|
:key="token.tokenId"
|
||||||
|
:label="token.name"
|
||||||
|
:value="token.tokenId"
|
||||||
|
:disabled="token.isDisabled"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- Model -->
|
||||||
|
<el-form-item label="模型" required>
|
||||||
|
<el-select
|
||||||
|
v-model="selectedModelId"
|
||||||
|
placeholder="请选择模型"
|
||||||
|
class="w-full"
|
||||||
|
:loading="modelLoading"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="model in modelOptions"
|
||||||
|
:key="model.modelId"
|
||||||
|
:label="model.modelName"
|
||||||
|
:value="model.modelId"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col py-1 max-w-[350px]">
|
||||||
|
<span class="font-medium truncate">{{ model.modelName }}</span>
|
||||||
|
<span class="text-xs text-gray-400 truncate" :title="model.modelDescribe">{{ model.modelDescribe }}</span>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- Prompt -->
|
||||||
|
<el-form-item label="提示词" required>
|
||||||
|
<template #label>
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<span>提示词</span>
|
||||||
|
<el-button link type="primary" size="small" @click="clearPrompt">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Delete />
|
||||||
|
</el-icon>清空
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-input
|
||||||
|
v-model="prompt"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 8, maxRows: 15 }"
|
||||||
|
placeholder="描述你想要生成的画面,例如:一只在太空中飞行的赛博朋克风格的猫..."
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
class="custom-textarea"
|
||||||
|
resize="vertical"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- Reference Image -->
|
||||||
|
<el-form-item label="参考图 (可选)">
|
||||||
|
<div class="w-full bg-gray-50 p-4 rounded-lg border border-dashed border-gray-300 hover:border-blue-400 transition-colors">
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
action="#"
|
||||||
|
list-type="picture-card"
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="2"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleRemove"
|
||||||
|
accept=".jpg,.jpeg,.png,.bmp,.webp"
|
||||||
|
:class="{ 'hide-upload-btn': fileList.length >= 2 }"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center justify-center text-gray-400">
|
||||||
|
<el-icon class="text-2xl mb-2">
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
|
<span class="text-xs">点击上传</span>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
<div class="text-xs text-gray-400 mt-2 flex justify-between items-center flex-wrap gap-2">
|
||||||
|
<span>最多2张,< 5MB (支持 JPG/PNG/WEBP)</span>
|
||||||
|
<el-checkbox v-model="compressImage" label="压缩图片" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-auto pt-4">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="w-full h-12 text-lg shadow-lg shadow-blue-500/30 transition-all hover:shadow-blue-500/50"
|
||||||
|
:loading="generating"
|
||||||
|
:disabled="!canGenerate"
|
||||||
|
round
|
||||||
|
@click="handleGenerate"
|
||||||
|
>
|
||||||
|
{{ generating ? '生成中...' : '开始生成' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Result Panel -->
|
||||||
|
<div class="flex-1 bg-gray-100 rounded-xl overflow-hidden relative flex flex-col h-full">
|
||||||
|
<div v-if="currentTask" class="flex-1 flex flex-col relative h-full overflow-hidden">
|
||||||
|
<!-- Image Display -->
|
||||||
|
<div class="flex-1 flex items-center justify-center p-4 bg-checkboard overflow-hidden relative min-h-0">
|
||||||
|
<div v-if="currentTask.taskStatus === 'Success' && currentTask.storeUrl" class="relative group w-full h-full flex items-center justify-center">
|
||||||
|
<el-image
|
||||||
|
ref="previewImageRef"
|
||||||
|
:src="currentTask.storeUrl"
|
||||||
|
fit="contain"
|
||||||
|
class="w-full h-full"
|
||||||
|
:style="{ maxHeight: '100%', maxWidth: '100%' }"
|
||||||
|
:preview-src-list="[currentTask.storeUrl]"
|
||||||
|
:initial-index="0"
|
||||||
|
:preview-teleported="true"
|
||||||
|
/>
|
||||||
|
<!-- Hover Actions -->
|
||||||
|
<div class="absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity flex gap-2 z-10">
|
||||||
|
<el-button circle type="primary" :icon="ZoomIn" @click="showViewer = true" />
|
||||||
|
<el-button circle type="primary" :icon="Download" @click="downloadImage" />
|
||||||
|
<el-button circle type="success" :icon="Refresh" title="重新生成" @click="handleGenerate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-image-viewer
|
||||||
|
v-if="showViewer"
|
||||||
|
|
||||||
|
fit="contain"
|
||||||
|
:url-list="[currentTask.storeUrl]"
|
||||||
|
@close="showViewer = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processing State -->
|
||||||
|
<div v-else-if="currentTask.taskStatus === 'Processing'" class="text-center">
|
||||||
|
<div class="loader mb-6" />
|
||||||
|
<p class="text-gray-600 font-medium text-lg animate-pulse">
|
||||||
|
正在绘制您的想象...
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-400 text-sm mt-2">
|
||||||
|
请稍候,这可能需要几秒钟
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fail State -->
|
||||||
|
<div v-else-if="currentTask.taskStatus === 'Fail'" class="text-center text-red-500 w-full max-w-lg">
|
||||||
|
<el-icon class="text-6xl mb-4">
|
||||||
|
<CircleCloseFilled />
|
||||||
|
</el-icon>
|
||||||
|
<p class="text-lg font-medium mb-2">
|
||||||
|
生成失败
|
||||||
|
</p>
|
||||||
|
<div class="bg-red-50 p-4 rounded-lg border border-red-100 text-sm text-left relative group/error">
|
||||||
|
<div class="max-h-32 overflow-y-auto custom-scrollbar break-words pr-6">
|
||||||
|
{{ currentTask.errorInfo || '请检查提示词或稍后重试' }}
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
class="absolute top-2 right-2 opacity-0 group-hover/error:opacity-100 transition-opacity"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
:icon="CopyDocument"
|
||||||
|
title="复制错误信息"
|
||||||
|
@click="copyError(currentTask.errorInfo || '')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-button class="mt-6" icon="Refresh" @click="handleGenerate">
|
||||||
|
重试
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prompt Display (Bottom) -->
|
||||||
|
<div class="bg-white p-4 border-t border-gray-200 shrink-0 max-h-40 overflow-y-auto">
|
||||||
|
<p class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
|
||||||
|
当前提示词
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-800 text-sm leading-relaxed whitespace-pre-wrap">
|
||||||
|
{{ currentTask.prompt }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else class="flex-1 flex flex-col items-center justify-center text-gray-400 bg-gray-50/50 h-full">
|
||||||
|
<div class="w-32 h-32 bg-gray-200 rounded-full flex items-center justify-center mb-6">
|
||||||
|
<el-icon class="text-5xl text-gray-400">
|
||||||
|
<PictureIcon />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-600 mb-2">
|
||||||
|
准备好开始了吗?
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-500 max-w-md text-center">
|
||||||
|
在左侧配置您的创意参数,点击生成按钮,见证AI的奇迹。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-checkboard {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loader Animation */
|
||||||
|
.loader {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 5px solid #e5e7eb;
|
||||||
|
border-bottom-color: #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: rotation 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotation {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hide-upload-btn .el-upload--picture-card) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
547
Yi.Ai.Vue3/src/pages/chat/image/components/ImagePlaza.vue
Normal file
547
Yi.Ai.Vue3/src/pages/chat/image/components/ImagePlaza.vue
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TaskItem, TaskListRequest } from '@/api/aiImage/types';
|
||||||
|
import {
|
||||||
|
CircleCloseFilled,
|
||||||
|
CollectionTag,
|
||||||
|
CopyDocument,
|
||||||
|
Download,
|
||||||
|
Filter,
|
||||||
|
Loading,
|
||||||
|
MagicStick,
|
||||||
|
Picture,
|
||||||
|
Refresh,
|
||||||
|
Search,
|
||||||
|
User,
|
||||||
|
WarningFilled,
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import { useClipboard } from '@vueuse/core';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { getImagePlaza } from '@/api/aiImage';
|
||||||
|
import TaskCard from './TaskCard.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['use-prompt', 'use-reference']);
|
||||||
|
|
||||||
|
const taskList = ref<TaskItem[]>([]);
|
||||||
|
const pageIndex = ref(1);
|
||||||
|
const pageSize = ref(20);
|
||||||
|
const loading = ref(false);
|
||||||
|
const noMore = ref(false);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const currentTask = ref<TaskItem | null>(null);
|
||||||
|
|
||||||
|
// Mobile filter drawer
|
||||||
|
const showMobileFilter = ref(false);
|
||||||
|
|
||||||
|
// Viewer State
|
||||||
|
const showViewer = ref(false);
|
||||||
|
const previewUrl = ref('');
|
||||||
|
|
||||||
|
// Filter State
|
||||||
|
const searchForm = reactive({
|
||||||
|
Prompt: '',
|
||||||
|
Categories: '',
|
||||||
|
UserName: '',
|
||||||
|
});
|
||||||
|
const dateRange = ref<[string, string] | null>(null);
|
||||||
|
|
||||||
|
const { copy } = useClipboard();
|
||||||
|
|
||||||
|
const disabled = computed(() => loading.value || noMore.value);
|
||||||
|
|
||||||
|
async function loadMore() {
|
||||||
|
if (loading.value || noMore.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params: TaskListRequest = {
|
||||||
|
SkipCount: (pageIndex.value - 1) * pageSize.value,
|
||||||
|
MaxResultCount: pageSize.value,
|
||||||
|
TaskStatus: 'Success',
|
||||||
|
Prompt: searchForm.Prompt || undefined,
|
||||||
|
Categories: searchForm.Categories || undefined,
|
||||||
|
UserName: searchForm.UserName || undefined,
|
||||||
|
StartTime: dateRange.value ? dateRange.value[0] : undefined,
|
||||||
|
EndTime: dateRange.value ? dateRange.value[1] : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await getImagePlaza(params);
|
||||||
|
|
||||||
|
// Handle potential wrapper
|
||||||
|
const data = (res as any).data || res;
|
||||||
|
const items = data.items || [];
|
||||||
|
const total = data.total || 0;
|
||||||
|
|
||||||
|
if (items.length < pageSize.value) {
|
||||||
|
noMore.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageIndex.value === 1) {
|
||||||
|
taskList.value = items;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Avoid duplicates
|
||||||
|
const newItems = items.filter((item: TaskItem) => !taskList.value.some(t => t.id === item.id));
|
||||||
|
taskList.value.push(...newItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we reached total
|
||||||
|
if (taskList.value.length >= total) {
|
||||||
|
noMore.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
pageIndex.value++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
noMore.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
noMore.value = true;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
pageIndex.value = 1;
|
||||||
|
taskList.value = [];
|
||||||
|
noMore.value = false;
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
searchForm.Prompt = '';
|
||||||
|
searchForm.Categories = '';
|
||||||
|
searchForm.UserName = '';
|
||||||
|
dateRange.value = null;
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCardClick(task: TaskItem) {
|
||||||
|
currentTask.value = task;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePreview(url: string) {
|
||||||
|
previewUrl.value = url;
|
||||||
|
showViewer.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeViewer() {
|
||||||
|
showViewer.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(time: string) {
|
||||||
|
try {
|
||||||
|
return format(new Date(time), 'yyyy-MM-dd HH:mm');
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyPrompt(text: string) {
|
||||||
|
await copy(text);
|
||||||
|
ElMessage.success('已复制到剪贴板');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadImage(url: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = blobUrl;
|
||||||
|
link.download = `image-${Date.now()}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Download failed', e);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose refresh method
|
||||||
|
defineExpose({
|
||||||
|
refresh: () => {
|
||||||
|
pageIndex.value = 1;
|
||||||
|
taskList.value = [];
|
||||||
|
noMore.value = false;
|
||||||
|
loadMore();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for filter changes - debounce for text inputs, immediate for others
|
||||||
|
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
watch(() => searchForm.Prompt, () => {
|
||||||
|
if (searchTimer) {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
}
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
handleSearch();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([() => searchForm.Categories, () => searchForm.UserName], () => {
|
||||||
|
if (searchTimer) {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
}
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
handleSearch();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(dateRange, () => {
|
||||||
|
handleSearch();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col md:flex-row bg-gray-50">
|
||||||
|
<!-- Mobile Filter Button -->
|
||||||
|
<div class="md:hidden p-4 bg-white border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<h2 class="text-lg font-bold text-gray-800">
|
||||||
|
图片广场
|
||||||
|
</h2>
|
||||||
|
<el-button type="primary" @click="showMobileFilter = true">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Filter />
|
||||||
|
</el-icon>
|
||||||
|
筛选
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Left Sidebar - Filters (Desktop) -->
|
||||||
|
<div class="hidden md:flex w-72 bg-white border-r border-gray-200 flex-col shadow-sm">
|
||||||
|
<div class="p-6 border-b border-gray-100">
|
||||||
|
<h2 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
||||||
|
<el-icon><Filter /></el-icon>
|
||||||
|
筛选条件
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" overflow-y-auto p-6 custom-scrollbar">
|
||||||
|
<el-form :model="searchForm" label-position="top" class="space-y-4">
|
||||||
|
<el-form-item label="提示词">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.Prompt"
|
||||||
|
placeholder="搜索提示词..."
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="分类标签">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.Categories"
|
||||||
|
placeholder="搜索分类..."
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><CollectionTag /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.UserName"
|
||||||
|
placeholder="搜索用户..."
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 border-t border-gray-100 space-y-2">
|
||||||
|
<el-button type="primary" class="w-full" @click="handleSearch">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Search />
|
||||||
|
</el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button class="w-full refresh-btn " @click="handleReset">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Refresh />
|
||||||
|
</el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Filter Drawer -->
|
||||||
|
<el-drawer
|
||||||
|
v-model="showMobileFilter"
|
||||||
|
title="筛选条件"
|
||||||
|
direction="ltr"
|
||||||
|
size="80%"
|
||||||
|
>
|
||||||
|
<el-form :model="searchForm" label-position="top" class="space-y-4">
|
||||||
|
<el-form-item label="提示词">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.Prompt"
|
||||||
|
placeholder="搜索提示词..."
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="分类标签">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.Categories"
|
||||||
|
placeholder="搜索分类..."
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><CollectionTag /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.UserName"
|
||||||
|
placeholder="搜索用户..."
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<el-button type="primary" class="w-full" @click="handleSearch(); showMobileFilter = false">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Search />
|
||||||
|
</el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button class="w-full" @click="handleReset(); showMobileFilter = false">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Refresh />
|
||||||
|
</el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
<!-- Right Content Area -->
|
||||||
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<div
|
||||||
|
v-infinite-scroll="loadMore"
|
||||||
|
class="flex-1 overflow-y-auto p-4 md:p-6 custom-scrollbar"
|
||||||
|
:infinite-scroll-disabled="disabled"
|
||||||
|
:infinite-scroll-distance="50"
|
||||||
|
>
|
||||||
|
<div v-if="taskList.length > 0" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||||
|
<TaskCard
|
||||||
|
v-for="task in taskList"
|
||||||
|
:key="task.id"
|
||||||
|
:task="task"
|
||||||
|
@click="handleCardClick(task)"
|
||||||
|
@use-prompt="$emit('use-prompt', $event)"
|
||||||
|
@use-reference="$emit('use-reference', $event)"
|
||||||
|
@preview="handlePreview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else-if="!loading && taskList.length === 0" class="h-full flex flex-col items-center justify-center text-gray-400">
|
||||||
|
<el-icon class="text-6xl mb-4">
|
||||||
|
<Picture />
|
||||||
|
</el-icon>
|
||||||
|
<p>暂无图片,快去生成一张吧!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="py-6 flex justify-center">
|
||||||
|
<div class="flex items-center gap-2 text-gray-500">
|
||||||
|
<el-icon class="is-loading">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No More State -->
|
||||||
|
<div v-if="noMore && taskList.length > 0" class="py-6 text-center text-gray-400 text-sm">
|
||||||
|
- 到底了,没有更多图片了 -
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dialog for details -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="图片详情"
|
||||||
|
width="900px"
|
||||||
|
append-to-body
|
||||||
|
class="image-detail-dialog"
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<div v-if="currentTask" class="flex flex-col md:flex-row gap-6 h-[600px]">
|
||||||
|
<!-- Left Image -->
|
||||||
|
<div class="flex-1 bg-black/5 rounded-lg flex items-center justify-center overflow-hidden relative group">
|
||||||
|
<el-image
|
||||||
|
v-if="currentTask.storeUrl"
|
||||||
|
:src="currentTask.storeUrl"
|
||||||
|
fit="contain"
|
||||||
|
class="w-full h-full"
|
||||||
|
:preview-src-list="[currentTask.storeUrl]"
|
||||||
|
/>
|
||||||
|
<div v-else class="flex flex-col items-center justify-center text-gray-400 p-4 text-center w-full h-full">
|
||||||
|
<span v-if="currentTask.taskStatus === 'Processing'" class="flex flex-col items-center">
|
||||||
|
<el-icon class="text-4xl mb-3 is-loading text-blue-500"><Loading /></el-icon>
|
||||||
|
<span class="text-blue-500 font-medium">生成中...</span>
|
||||||
|
</span>
|
||||||
|
<div v-else-if="currentTask.taskStatus === 'Fail'" class="flex flex-col items-center w-full max-w-md px-4">
|
||||||
|
<el-icon class="text-5xl mb-4 text-red-500">
|
||||||
|
<CircleCloseFilled />
|
||||||
|
</el-icon>
|
||||||
|
<span class="font-bold mb-4 text-lg text-red-600">生成失败</span>
|
||||||
|
<div class="w-full bg-gradient-to-br from-red-50 to-red-100 border-2 border-red-200 rounded-xl p-4 relative shadow-sm">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<el-icon class="text-red-500 mt-0.5 flex-shrink-0">
|
||||||
|
<WarningFilled />
|
||||||
|
</el-icon>
|
||||||
|
<div class="flex-1 text-sm text-red-800 max-h-40 overflow-y-auto custom-scrollbar break-words text-left leading-relaxed">
|
||||||
|
{{ currentTask.errorInfo || '未知错误,请稍后重试' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
class="absolute top-2 right-2"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
type="danger"
|
||||||
|
:icon="CopyDocument"
|
||||||
|
title="复制错误信息"
|
||||||
|
@click="copyPrompt(currentTask.errorInfo || '未知错误')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Download Button Overlay -->
|
||||||
|
<div v-if="currentTask.storeUrl" class="absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity z-10">
|
||||||
|
<el-button circle type="primary" :icon="Download" @click="downloadImage(currentTask.storeUrl)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Info -->
|
||||||
|
<div class="w-full md:w-[300px] flex flex-col gap-4 overflow-hidden">
|
||||||
|
<div class="flex-1 flex flex-col min-h-0">
|
||||||
|
<h3 class="font-bold text-gray-800 mb-2 flex items-center gap-2 shrink-0">
|
||||||
|
<el-icon><MagicStick /></el-icon> 提示词
|
||||||
|
</h3>
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg border border-gray-100 text-sm text-gray-600 leading-relaxed relative group/prompt overflow-y-auto custom-scrollbar flex-1">
|
||||||
|
{{ currentTask.prompt }}
|
||||||
|
<el-button
|
||||||
|
class="absolute top-2 right-2 opacity-0 group-hover/prompt:opacity-100 transition-opacity shadow-sm z-10"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
:icon="CopyDocument"
|
||||||
|
title="复制提示词"
|
||||||
|
@click="copyPrompt(currentTask.prompt)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-auto space-y-3 pt-4 border-t border-gray-100 shrink-0">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-gray-500">创建时间</span>
|
||||||
|
<span class="text-gray-800">{{ formatTime(currentTask.creationTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-gray-500">状态</span>
|
||||||
|
<el-tag size="small" type="success">
|
||||||
|
生成成功
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-1 mt-2">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="MagicStick"
|
||||||
|
@click="$emit('use-prompt', currentTask.prompt)"
|
||||||
|
>
|
||||||
|
使用提示词
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="false"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:icon="Picture"
|
||||||
|
@click="$emit('use-reference', currentTask.storeUrl)"
|
||||||
|
>
|
||||||
|
做参考图
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- Global Image Viewer -->
|
||||||
|
<el-image-viewer
|
||||||
|
v-if="showViewer && previewUrl"
|
||||||
|
:url-list="[previewUrl]"
|
||||||
|
@close="closeViewer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.refresh-btn {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
699
Yi.Ai.Vue3/src/pages/chat/image/components/MyImages.vue
Normal file
699
Yi.Ai.Vue3/src/pages/chat/image/components/MyImages.vue
Normal file
@@ -0,0 +1,699 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { InputInstance } from 'element-plus';
|
||||||
|
import type { TaskItem, TaskListRequest } from '@/api/aiImage/types';
|
||||||
|
import { Check, CircleCloseFilled, CopyDocument, Download, Filter, Loading, MagicStick, Picture, Refresh, Search, Share, WarningFilled } from '@element-plus/icons-vue';
|
||||||
|
import { useClipboard } from '@vueuse/core';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||||
|
import { getMyTasks, publishImage } from '@/api/aiImage';
|
||||||
|
import TaskCard from './TaskCard.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['use-prompt', 'use-reference']);
|
||||||
|
|
||||||
|
const taskList = ref<TaskItem[]>([]);
|
||||||
|
const pageIndex = ref(1);
|
||||||
|
const pageSize = ref(20);
|
||||||
|
const loading = ref(false);
|
||||||
|
const noMore = ref(false);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const currentTask = ref<TaskItem | null>(null);
|
||||||
|
|
||||||
|
// Mobile filter drawer
|
||||||
|
const showMobileFilter = ref(false);
|
||||||
|
|
||||||
|
// Viewer State
|
||||||
|
const showViewer = ref(false);
|
||||||
|
const previewUrl = ref('');
|
||||||
|
|
||||||
|
// Filter State
|
||||||
|
const searchForm = reactive({
|
||||||
|
Prompt: '',
|
||||||
|
TaskStatus: '' as 'Processing' | 'Success' | 'Fail' | '',
|
||||||
|
PublishStatus: '' as 'Unpublished' | 'Published' | '',
|
||||||
|
});
|
||||||
|
const dateRange = ref<[string, string] | null>(null);
|
||||||
|
|
||||||
|
// Publish Dialog State
|
||||||
|
const publishDialogVisible = ref(false);
|
||||||
|
const publishing = ref(false);
|
||||||
|
const publishTags = ref<string[]>([]);
|
||||||
|
const inputValue = ref('');
|
||||||
|
const inputVisible = ref(false);
|
||||||
|
const InputRef = ref<InputInstance>();
|
||||||
|
const taskToPublish = ref<TaskItem | null>(null);
|
||||||
|
const isAnonymousPublish = ref(false);
|
||||||
|
|
||||||
|
const { copy } = useClipboard();
|
||||||
|
|
||||||
|
const disabled = computed(() => loading.value || noMore.value);
|
||||||
|
|
||||||
|
async function loadMore() {
|
||||||
|
if (loading.value || noMore.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params: TaskListRequest = {
|
||||||
|
SkipCount: (pageIndex.value - 1) * pageSize.value,
|
||||||
|
MaxResultCount: pageSize.value,
|
||||||
|
Prompt: searchForm.Prompt || undefined,
|
||||||
|
TaskStatus: searchForm.TaskStatus || undefined,
|
||||||
|
PublishStatus: searchForm.PublishStatus || undefined,
|
||||||
|
StartTime: dateRange.value ? dateRange.value[0] : undefined,
|
||||||
|
EndTime: dateRange.value ? dateRange.value[1] : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await getMyTasks(params);
|
||||||
|
|
||||||
|
// Handle potential wrapper
|
||||||
|
const data = (res as any).data || res;
|
||||||
|
const items = data.items || [];
|
||||||
|
const total = data.total || 0;
|
||||||
|
|
||||||
|
if (items.length < pageSize.value) {
|
||||||
|
noMore.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageIndex.value === 1) {
|
||||||
|
taskList.value = items;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Avoid duplicates if any
|
||||||
|
const newItems = items.filter((item: TaskItem) => !taskList.value.some(t => t.id === item.id));
|
||||||
|
taskList.value.push(...newItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we reached total
|
||||||
|
if (taskList.value.length >= total) {
|
||||||
|
noMore.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
pageIndex.value++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
noMore.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
// Stop trying if error occurs to prevent loop
|
||||||
|
noMore.value = true;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
pageIndex.value = 1;
|
||||||
|
taskList.value = [];
|
||||||
|
noMore.value = false;
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
searchForm.Prompt = '';
|
||||||
|
searchForm.TaskStatus = '';
|
||||||
|
searchForm.PublishStatus = '';
|
||||||
|
dateRange.value = null;
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCardClick(task: TaskItem) {
|
||||||
|
currentTask.value = task;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePreview(url: string) {
|
||||||
|
previewUrl.value = url;
|
||||||
|
showViewer.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeViewer() {
|
||||||
|
showViewer.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(time: string) {
|
||||||
|
try {
|
||||||
|
return format(new Date(time), 'yyyy-MM-dd HH:mm');
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyPrompt(text: string) {
|
||||||
|
await copy(text);
|
||||||
|
ElMessage.success('提示词已复制');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPublishDialog(task: TaskItem) {
|
||||||
|
taskToPublish.value = task;
|
||||||
|
publishTags.value = [];
|
||||||
|
inputValue.value = '';
|
||||||
|
inputVisible.value = false;
|
||||||
|
isAnonymousPublish.value = false;
|
||||||
|
publishDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCloseTag(tag: string) {
|
||||||
|
publishTags.value.splice(publishTags.value.indexOf(tag), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showInput() {
|
||||||
|
inputVisible.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
InputRef.value!.input!.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputConfirm() {
|
||||||
|
if (inputValue.value) {
|
||||||
|
if (!publishTags.value.includes(inputValue.value)) {
|
||||||
|
publishTags.value.push(inputValue.value);
|
||||||
|
}
|
||||||
|
inputValue.value = '';
|
||||||
|
// 保持焦点
|
||||||
|
nextTick(() => {
|
||||||
|
InputRef.value?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmPublish() {
|
||||||
|
if (!taskToPublish.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
publishing.value = true;
|
||||||
|
try {
|
||||||
|
await publishImage({
|
||||||
|
taskId: taskToPublish.value.id,
|
||||||
|
categories: publishTags.value,
|
||||||
|
isAnonymous: isAnonymousPublish.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
ElMessage.success('发布成功');
|
||||||
|
taskToPublish.value.publishStatus = 'Published';
|
||||||
|
publishDialogVisible.value = false;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
ElMessage.error('发布失败');
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
publishing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadImage(url: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = blobUrl;
|
||||||
|
link.download = `image-${Date.now()}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Download failed', e);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose refresh method
|
||||||
|
defineExpose({
|
||||||
|
refresh: () => {
|
||||||
|
handleSearch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for filter changes - debounce for prompt, immediate for others
|
||||||
|
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
watch(() => searchForm.Prompt, () => {
|
||||||
|
if (searchTimer) {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
}
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
handleSearch();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([() => searchForm.TaskStatus, () => searchForm.PublishStatus, dateRange], () => {
|
||||||
|
handleSearch();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col md:flex-row bg-gray-50">
|
||||||
|
<!-- Mobile Filter Button -->
|
||||||
|
<div class="md:hidden p-4 bg-white border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<h2 class="text-lg font-bold text-gray-800">
|
||||||
|
我的图库
|
||||||
|
</h2>
|
||||||
|
<el-button type="primary" @click="showMobileFilter = true">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Filter />
|
||||||
|
</el-icon>
|
||||||
|
筛选
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Left Sidebar - Filters (Desktop) -->
|
||||||
|
<div class="hidden md:flex w-72 bg-white border-r border-gray-200 flex-col shadow-sm">
|
||||||
|
<div class="p-6 border-b border-gray-100">
|
||||||
|
<h2 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
||||||
|
<el-icon><Filter /></el-icon>
|
||||||
|
筛选条件
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" overflow-y-auto p-6 custom-scrollbar">
|
||||||
|
<el-form :model="searchForm" label-position="top" class="space-y-4">
|
||||||
|
<el-form-item label="提示词">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.Prompt"
|
||||||
|
placeholder="搜索提示词..."
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="任务状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.TaskStatus"
|
||||||
|
placeholder="全部状态"
|
||||||
|
class="w-full"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option label="进行中" value="Processing">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-icon class="text-blue-500">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
<span>进行中</span>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="成功" value="Success">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-icon class="text-green-500">
|
||||||
|
<Check />
|
||||||
|
</el-icon>
|
||||||
|
<span>成功</span>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
<el-option label="失败" value="Fail">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-icon class="text-red-500">
|
||||||
|
<CircleCloseFilled />
|
||||||
|
</el-icon>
|
||||||
|
<span>失败</span>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="发布状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.PublishStatus"
|
||||||
|
placeholder="全部状态"
|
||||||
|
class="w-full"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option label="未发布" value="Unpublished" />
|
||||||
|
<el-option label="已发布" value="Published" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 border-t border-gray-100 space-y-2">
|
||||||
|
<el-button type="primary" class="w-full" @click="handleSearch">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Search />
|
||||||
|
</el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button class="w-full refresh-btn" @click="handleReset">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Refresh />
|
||||||
|
</el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Filter Drawer -->
|
||||||
|
<el-drawer
|
||||||
|
v-model="showMobileFilter"
|
||||||
|
title="筛选条件"
|
||||||
|
direction="ltr"
|
||||||
|
size="80%"
|
||||||
|
>
|
||||||
|
<el-form :model="searchForm" label-position="top" class="space-y-4">
|
||||||
|
<el-form-item label="提示词">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.Prompt"
|
||||||
|
placeholder="搜索提示词..."
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="任务状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.TaskStatus"
|
||||||
|
placeholder="全部状态"
|
||||||
|
class="w-full"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option label="进行中" value="Processing" />
|
||||||
|
<el-option label="成功" value="Success" />
|
||||||
|
<el-option label="失败" value="Fail" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="发布状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.PublishStatus"
|
||||||
|
placeholder="全部状态"
|
||||||
|
class="w-full"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option label="未发布" value="Unpublished" />
|
||||||
|
<el-option label="已发布" value="Published" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<el-button type="primary" class="w-full" @click="handleSearch(); showMobileFilter = false">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Search />
|
||||||
|
</el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button class="w-full" @click="handleReset(); showMobileFilter = false">
|
||||||
|
<el-icon class="mr-1">
|
||||||
|
<Refresh />
|
||||||
|
</el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
<!-- Right Content Area -->
|
||||||
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<div
|
||||||
|
v-infinite-scroll="loadMore"
|
||||||
|
class="flex-1 overflow-y-auto p-4 md:p-6 custom-scrollbar"
|
||||||
|
:infinite-scroll-disabled="disabled"
|
||||||
|
:infinite-scroll-distance="50"
|
||||||
|
>
|
||||||
|
<div v-if="taskList.length > 0" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||||
|
<TaskCard
|
||||||
|
v-for="task in taskList"
|
||||||
|
:key="task.id"
|
||||||
|
:task="task"
|
||||||
|
show-publish-status
|
||||||
|
@click="handleCardClick(task)"
|
||||||
|
@use-prompt="$emit('use-prompt', $event)"
|
||||||
|
@use-reference="$emit('use-reference', $event)"
|
||||||
|
@publish="openPublishDialog(task)"
|
||||||
|
@preview="handlePreview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else-if="!loading && taskList.length === 0" class="h-full flex flex-col items-center justify-center text-gray-400">
|
||||||
|
<el-icon class="text-6xl mb-4">
|
||||||
|
<Picture />
|
||||||
|
</el-icon>
|
||||||
|
<p>没有找到相关图片</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="py-6 flex justify-center">
|
||||||
|
<div class="flex items-center gap-2 text-gray-500">
|
||||||
|
<el-icon class="is-loading">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No More State -->
|
||||||
|
<div v-if="noMore && taskList.length > 0" class="py-6 text-center text-gray-400 text-sm">
|
||||||
|
- 到底了,没有更多图片了 -
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dialog for details -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="图片详情"
|
||||||
|
width="900px"
|
||||||
|
append-to-body
|
||||||
|
class="image-detail-dialog"
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<div v-if="currentTask" class="flex flex-col md:flex-row gap-6 h-[600px]">
|
||||||
|
<!-- Left Image -->
|
||||||
|
<div class="flex-1 bg-black/5 rounded-lg flex items-center justify-center overflow-hidden relative group">
|
||||||
|
<el-image
|
||||||
|
v-if="currentTask.storeUrl"
|
||||||
|
:src="currentTask.storeUrl"
|
||||||
|
fit="contain"
|
||||||
|
class="w-full h-full"
|
||||||
|
:preview-src-list="[currentTask.storeUrl]"
|
||||||
|
/>
|
||||||
|
<div v-else class="flex flex-col items-center justify-center text-gray-400 p-4 text-center w-full h-full">
|
||||||
|
<span v-if="currentTask.taskStatus === 'Processing'" class="flex flex-col items-center">
|
||||||
|
<el-icon class="text-4xl mb-3 is-loading text-blue-500"><Loading /></el-icon>
|
||||||
|
<span class="text-blue-500 font-medium">生成中...</span>
|
||||||
|
</span>
|
||||||
|
<div v-else-if="currentTask.taskStatus === 'Fail'" class="flex flex-col items-center w-full max-w-md px-4">
|
||||||
|
<el-icon class="text-5xl mb-4 text-red-500">
|
||||||
|
<CircleCloseFilled />
|
||||||
|
</el-icon>
|
||||||
|
<span class="font-bold mb-4 text-lg text-red-600">生成失败</span>
|
||||||
|
<div class="w-full bg-gradient-to-br from-red-50 to-red-100 border-2 border-red-200 rounded-xl p-4 relative shadow-sm">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<el-icon class="text-red-500 mt-0.5 flex-shrink-0">
|
||||||
|
<WarningFilled />
|
||||||
|
</el-icon>
|
||||||
|
<div class="flex-1 text-sm text-red-800 max-h-40 overflow-y-auto custom-scrollbar break-words text-left leading-relaxed">
|
||||||
|
{{ currentTask.errorInfo || '未知错误,请稍后重试' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
class="absolute top-2 right-2"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
type="danger"
|
||||||
|
:icon="CopyDocument"
|
||||||
|
title="复制错误信息"
|
||||||
|
@click="copyErrorInfo(currentTask.errorInfo || '未知错误')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Download Button Overlay -->
|
||||||
|
<div v-if="currentTask.storeUrl" class="absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity z-10">
|
||||||
|
<el-button circle type="primary" :icon="Download" @click="downloadImage(currentTask.storeUrl)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Info -->
|
||||||
|
<div class="w-full md:w-[300px] flex flex-col gap-4 overflow-hidden">
|
||||||
|
<div class="flex-1 flex flex-col min-h-0">
|
||||||
|
<h3 class="font-bold text-gray-800 mb-2 flex items-center gap-2 shrink-0">
|
||||||
|
<el-icon><MagicStick /></el-icon> 提示词
|
||||||
|
</h3>
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg border border-gray-100 text-sm text-gray-600 leading-relaxed relative group/prompt overflow-y-auto custom-scrollbar flex-1">
|
||||||
|
{{ currentTask.prompt }}
|
||||||
|
<el-button
|
||||||
|
class="absolute top-2 right-2 opacity-0 group-hover/prompt:opacity-100 transition-opacity shadow-sm z-10"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
:icon="CopyDocument"
|
||||||
|
title="复制提示词"
|
||||||
|
@click="copyPrompt(currentTask.prompt)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-auto space-y-3 pt-4 border-t border-gray-100 shrink-0">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-gray-500">创建时间</span>
|
||||||
|
<span class="text-gray-800">{{ formatTime(currentTask.creationTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-sm items-center">
|
||||||
|
<span class="text-gray-500">状态</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-tag v-if="currentTask.taskStatus === 'Success'" size="small" type="success">
|
||||||
|
成功
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else-if="currentTask.taskStatus === 'Processing'" size="small" type="primary">
|
||||||
|
进行中
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else size="small" type="danger">
|
||||||
|
失败
|
||||||
|
</el-tag>
|
||||||
|
|
||||||
|
<el-tag v-if="currentTask.publishStatus === 'Published'" size="small" type="warning" effect="dark">
|
||||||
|
已发布
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentTask.taskStatus === 'Success'" class="pt-2 space-y-2">
|
||||||
|
<div class="grid grid-cols-1 gap-1">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:icon="MagicStick"
|
||||||
|
@click="$emit('use-prompt', currentTask.prompt)"
|
||||||
|
>
|
||||||
|
使用提示词
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="false"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:icon="Picture"
|
||||||
|
@click="$emit('use-reference', currentTask.storeUrl)"
|
||||||
|
>
|
||||||
|
做参考图
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
v-if="currentTask.publishStatus === 'Unpublished'"
|
||||||
|
type="success"
|
||||||
|
class="w-full"
|
||||||
|
:icon="Share"
|
||||||
|
@click="openPublishDialog(currentTask)"
|
||||||
|
>
|
||||||
|
发布到广场
|
||||||
|
</el-button>
|
||||||
|
<el-button v-else type="info" disabled class="w-full">
|
||||||
|
已发布
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- Publish Dialog -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="publishDialogVisible"
|
||||||
|
title="发布到广场"
|
||||||
|
width="500px"
|
||||||
|
append-to-body
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="标签 (回车添加)">
|
||||||
|
<div class="flex gap-2 flex-wrap w-full p-2 border border-gray-200 rounded-md min-h-[40px]">
|
||||||
|
<el-tag
|
||||||
|
v-for="tag in publishTags"
|
||||||
|
:key="tag"
|
||||||
|
closable
|
||||||
|
:disable-transitions="false"
|
||||||
|
@close="handleCloseTag(tag)"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
<el-input
|
||||||
|
v-if="inputVisible"
|
||||||
|
ref="InputRef"
|
||||||
|
v-model="inputValue"
|
||||||
|
class="w-24"
|
||||||
|
size="small"
|
||||||
|
@keydown.enter.prevent="handleInputConfirm"
|
||||||
|
@blur="handleInputConfirm"
|
||||||
|
/>
|
||||||
|
<el-button v-else class="button-new-tag" size="small" @click="showInput">
|
||||||
|
+ New Tag
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="isAnonymousPublish" label="匿名发布" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="publishDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="publishing" @click="confirmPublish">
|
||||||
|
发布
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- Global Image Viewer -->
|
||||||
|
<el-image-viewer
|
||||||
|
v-if="showViewer && previewUrl"
|
||||||
|
:url-list="[previewUrl]"
|
||||||
|
@close="closeViewer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.refresh-btn {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
183
Yi.Ai.Vue3/src/pages/chat/image/components/TaskCard.vue
Normal file
183
Yi.Ai.Vue3/src/pages/chat/image/components/TaskCard.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TaskItem } from '@/api/aiImage/types';
|
||||||
|
import { Check, CircleCloseFilled, Download, Loading, MagicStick, Picture, Picture as PictureIcon, Share, User, ZoomIn } from '@element-plus/icons-vue';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { defineEmits, defineProps } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
task: TaskItem;
|
||||||
|
showPublishStatus?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['click', 'use-prompt', 'use-reference', 'publish', 'preview']);
|
||||||
|
|
||||||
|
function formatTime(time: string) {
|
||||||
|
try {
|
||||||
|
return format(new Date(time), 'MM-dd HH:mm');
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePreview() {
|
||||||
|
if (props.task.storeUrl) {
|
||||||
|
emit('preview', props.task.storeUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDownload() {
|
||||||
|
if (!props.task.storeUrl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(props.task.storeUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = blobUrl;
|
||||||
|
link.download = `image-${Date.now()}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Download failed', e);
|
||||||
|
window.open(props.task.storeUrl, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="task-card group relative flex flex-col bg-white rounded-xl overflow-hidden border border-gray-100 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 cursor-pointer">
|
||||||
|
<!-- Image Area -->
|
||||||
|
<div class="aspect-square w-full relative bg-gray-100 overflow-hidden">
|
||||||
|
<!-- Blurred Background -->
|
||||||
|
<div
|
||||||
|
v-if="task.taskStatus === 'Success' && task.storeUrl"
|
||||||
|
class="absolute inset-0 bg-cover bg-center blur-xl opacity-60 scale-125"
|
||||||
|
:style="{ backgroundImage: `url(${task.storeUrl})` }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-image
|
||||||
|
v-if="task.taskStatus === 'Success' && task.storeUrl"
|
||||||
|
:src="task.storeUrl"
|
||||||
|
fit="contain"
|
||||||
|
loading="lazy"
|
||||||
|
class="w-full h-full relative z-10 transition-transform duration-500 group-hover:scale-105"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="flex flex-col justify-center items-center w-full h-full text-gray-400 bg-gray-50">
|
||||||
|
<el-icon class="text-3xl mb-2">
|
||||||
|
<Picture />
|
||||||
|
</el-icon>
|
||||||
|
<span class="text-xs">加载失败</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="flex justify-center items-center w-full h-full bg-gray-50">
|
||||||
|
<el-icon class="is-loading text-gray-400">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
|
||||||
|
<!-- Non-Success States -->
|
||||||
|
<div v-else class="w-full h-full flex flex-col justify-center items-center p-4 text-center relative z-10 bg-gray-50">
|
||||||
|
<div v-if="task.taskStatus === 'Processing'" class="flex flex-col items-center text-blue-500">
|
||||||
|
<el-icon class="is-loading text-2xl mb-2">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
<span class="text-xs font-medium">生成中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="task.taskStatus === 'Fail'" class="flex flex-col items-center text-red-500">
|
||||||
|
<el-icon class="text-2xl mb-2">
|
||||||
|
<CircleCloseFilled />
|
||||||
|
</el-icon>
|
||||||
|
<span class="text-xs font-medium">生成失败</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-gray-400">
|
||||||
|
<span class="text-xs">等待中</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay (Hover) -->
|
||||||
|
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col items-center justify-center gap-3 z-20 backdrop-blur-[2px]">
|
||||||
|
<el-button type="primary" round size="small" class="transform scale-90 group-hover:scale-100 transition-transform shadow-lg" @click.stop="$emit('click')">
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
<div v-if="task.taskStatus === 'Success'" class="flex gap-2">
|
||||||
|
<el-tooltip content="放大查看" placement="top" :show-after="500">
|
||||||
|
<el-button circle size="small" :icon="ZoomIn" @click.stop="handlePreview" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="下载图片" placement="top" :show-after="500">
|
||||||
|
<el-button circle size="small" :icon="Download" @click.stop="handleDownload" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="使用提示词" placement="top" :show-after="500">
|
||||||
|
<el-button circle size="small" :icon="MagicStick" @click.stop="$emit('use-prompt', task.prompt)" />
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-tooltip v-if="false" content="做参考图" placement="top" :show-after="500">
|
||||||
|
<el-button circle size="small" :icon="PictureIcon" @click.stop="$emit('use-reference', task.storeUrl)" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip v-if="task.publishStatus === 'Unpublished'" content="发布到广场" placement="top" :show-after="500">
|
||||||
|
<el-button circle size="small" type="success" :icon="Share" @click.stop="$emit('publish', task)" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Tag -->
|
||||||
|
<div v-if="showPublishStatus && task.publishStatus === 'Published'" class="absolute top-2 right-2 z-20">
|
||||||
|
<el-tag type="success" effect="dark" size="small" round>
|
||||||
|
已发布
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="p-3 flex-1 flex flex-col">
|
||||||
|
<p class="text-sm text-gray-700 line-clamp-2 mb-2 h-10 leading-5" :title="task.prompt">
|
||||||
|
{{ task.prompt }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
<div v-if="task.categories && task.categories.length > 0" class="flex flex-wrap gap-1 mb-2">
|
||||||
|
<el-tag
|
||||||
|
v-for="tag in task.categories.slice(0, 3)"
|
||||||
|
:key="tag"
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-if="task.categories.length > 3" size="small" type="info" effect="plain">
|
||||||
|
+{{ task.categories.length - 3 }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-auto flex justify-between items-center text-xs text-gray-400">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span>{{ formatTime(task.creationTime) }}</span>
|
||||||
|
<span v-if="!task.isAnonymous && task.userName" class="text-blue-500 flex items-center gap-1">
|
||||||
|
<el-icon><User /></el-icon> {{ task.userName }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="task.isAnonymous" class="text-gray-400 flex items-center gap-1">
|
||||||
|
<el-icon><User /></el-icon> 匿名用户
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="task.taskStatus === 'Success'" class="text-green-500 flex items-center gap-1">
|
||||||
|
<el-icon><Check /></el-icon> 完成
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.task-card {
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,26 +1,87 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 图片生成功能 - 预留
|
import { nextTick, ref, watch } from 'vue';
|
||||||
|
import ImageGenerator from './components/ImageGenerator.vue';
|
||||||
|
import ImagePlaza from './components/ImagePlaza.vue';
|
||||||
|
import MyImages from './components/MyImages.vue';
|
||||||
|
|
||||||
|
const activeTab = ref('plaza');
|
||||||
|
const myImagesRef = ref();
|
||||||
|
const imageGeneratorRef = ref();
|
||||||
|
|
||||||
|
function handleTaskCreated() {
|
||||||
|
// Optional: Switch to My Images or just notify
|
||||||
|
// For now, we stay on Generator page to see the result.
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUsePrompt(prompt: string) {
|
||||||
|
activeTab.value = 'generate';
|
||||||
|
nextTick(() => {
|
||||||
|
if (imageGeneratorRef.value) {
|
||||||
|
imageGeneratorRef.value.setPrompt(prompt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUseReference(url: string) {
|
||||||
|
activeTab.value = 'generate';
|
||||||
|
nextTick(() => {
|
||||||
|
if (imageGeneratorRef.value && url) {
|
||||||
|
imageGeneratorRef.value.addReferenceImage(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh My Images when tab is activated
|
||||||
|
watch(activeTab, (val) => {
|
||||||
|
if (val === 'my-images' && myImagesRef.value) {
|
||||||
|
myImagesRef.value.refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="image-generation-page">
|
<div class="image-page-container h-full flex flex-col">
|
||||||
<el-empty description="图片生成功能开发中,敬请期待">
|
<el-tabs v-model="activeTab" class="flex-1 flex flex-col" type="border-card">
|
||||||
<template #image>
|
<el-tab-pane label="图片广场" name="plaza" class="h-full">
|
||||||
<el-icon style="font-size: 80px; color: var(--el-color-primary);">
|
<ImagePlaza
|
||||||
<i-ep-picture />
|
v-if="activeTab === 'plaza'"
|
||||||
</el-icon>
|
@use-prompt="handleUsePrompt"
|
||||||
</template>
|
@use-reference="handleUseReference"
|
||||||
</el-empty>
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="图片生成" name="generate" class="h-full">
|
||||||
|
<ImageGenerator
|
||||||
|
ref="imageGeneratorRef"
|
||||||
|
:is-active="activeTab === 'generate'"
|
||||||
|
@task-created="handleTaskCreated"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="我的图库" name="my-images" class="h-full">
|
||||||
|
<MyImages
|
||||||
|
v-if="activeTab === 'my-images'"
|
||||||
|
ref="myImagesRef"
|
||||||
|
@use-prompt="handleUsePrompt"
|
||||||
|
@use-reference="handleUseReference"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped>
|
||||||
.image-generation-page {
|
.image-page-container {
|
||||||
display: flex;
|
height: 100%;
|
||||||
align-items: center;
|
box-sizing: border-box;
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
}
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
|
||||||
|
}
|
||||||
|
:deep(.el-tab-pane) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--el-bg-color);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 控制侧边栏折叠状态
|
// 控制侧边栏折叠状态
|
||||||
const isCollapsed = ref(false);
|
const isCollapsed = ref(true);
|
||||||
|
|
||||||
// 菜单项配置
|
// 菜单项配置
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: 'conversation', label: '对话', icon: 'ChatDotRound', path: '/chat/conversation' },
|
{ name: 'conversation', label: 'AI对话', icon: 'ChatDotRound', path: '/chat/conversation' },
|
||||||
{ name: 'image', label: '图片生成', icon: 'Picture', path: '/chat/image' },
|
{ name: 'image', label: 'AI图片', icon: 'Picture', path: '/chat/image' },
|
||||||
{ name: 'video', label: '视频生成', icon: 'VideoCamera', path: '/chat/video' },
|
{ name: 'video', label: 'AI视频', icon: 'VideoCamera', path: '/chat/video' },
|
||||||
|
{ name: 'monitor', label: 'AI智能体', icon: 'Monitor', path: '/chat/agent' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 当前激活的菜单
|
// 当前激活的菜单
|
||||||
@@ -47,7 +48,7 @@ window.addEventListener('resize', checkIsMobile);
|
|||||||
<div class="console-page" :class="{ 'is-collapsed': isCollapsed }">
|
<div class="console-page" :class="{ 'is-collapsed': isCollapsed }">
|
||||||
<!-- 侧边栏导航 -->
|
<!-- 侧边栏导航 -->
|
||||||
<div class="nav-sidebar" :class="{ 'is-collapsed': isCollapsed }">
|
<div class="nav-sidebar" :class="{ 'is-collapsed': isCollapsed }">
|
||||||
<div class="nav-header">
|
<div v-if="false" class="nav-header">
|
||||||
<h2 v-show="!isCollapsed" class="nav-title">
|
<h2 v-show="!isCollapsed" class="nav-title">
|
||||||
AI聊天
|
AI聊天
|
||||||
</h2>
|
</h2>
|
||||||
@@ -92,7 +93,7 @@ window.addEventListener('resize', checkIsMobile);
|
|||||||
<div v-if="isMobile" class="content-header">
|
<div v-if="isMobile" class="content-header">
|
||||||
<div class="mobile-toggle" @click="isCollapsed = false">
|
<div class="mobile-toggle" @click="isCollapsed = false">
|
||||||
<el-icon><i-ep-expand /></el-icon>
|
<el-icon><i-ep-expand /></el-icon>
|
||||||
<span>菜单</span>
|
<span>AI应用</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ watch(
|
|||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1000px;
|
//max-width: 1000px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -607,7 +607,7 @@ function handleImagePreview(url: string) {
|
|||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1000px;
|
//max-width: 1000px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -2,15 +2,29 @@
|
|||||||
import { Expand, Fold } from '@element-plus/icons-vue';
|
import { Expand, Fold } from '@element-plus/icons-vue';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { checkPagePermission } from '@/config/permission.ts';
|
||||||
|
import { useUserStore } from '@/stores';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 从 URL 中提取邀请码参数
|
||||||
|
const inviteCodeFromUrl = computed(() => {
|
||||||
|
return route.query.inviteCode as string | undefined;
|
||||||
|
});
|
||||||
|
|
||||||
// 控制侧边栏折叠状态
|
// 控制侧边栏折叠状态
|
||||||
const isCollapsed = ref(false);
|
const isCollapsed = ref(false);
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const userName = userStore.userInfo?.user?.userName;
|
||||||
|
|
||||||
|
const hasPermission = checkPagePermission('/console/channel', userName);
|
||||||
|
|
||||||
// 菜单项配置
|
// 菜单项配置
|
||||||
const navItems = [
|
|
||||||
|
// 基础菜单项
|
||||||
|
const baseNavItems = [
|
||||||
{ name: 'user', label: '用户信息', icon: 'User', path: '/console/user' },
|
{ name: 'user', label: '用户信息', icon: 'User', path: '/console/user' },
|
||||||
{ name: 'apikey', label: 'API密钥', icon: 'Key', path: '/console/apikey' },
|
{ name: 'apikey', label: 'API密钥', icon: 'Key', path: '/console/apikey' },
|
||||||
{ name: 'recharge-log', label: '充值记录', icon: 'Document', path: '/console/recharge-log' },
|
{ name: 'recharge-log', label: '充值记录', icon: 'Document', path: '/console/recharge-log' },
|
||||||
@@ -19,9 +33,13 @@ const navItems = [
|
|||||||
{ name: 'daily-task', label: '每日任务(限时)', icon: 'Trophy', path: '/console/daily-task' },
|
{ name: 'daily-task', label: '每日任务(限时)', icon: 'Trophy', path: '/console/daily-task' },
|
||||||
{ name: 'invite', label: '每周邀请(限时)', icon: 'Present', path: '/console/invite' },
|
{ name: 'invite', label: '每周邀请(限时)', icon: 'Present', path: '/console/invite' },
|
||||||
{ name: 'activation', label: '激活码兑换', icon: 'MagicStick', path: '/console/activation' },
|
{ name: 'activation', label: '激活码兑换', icon: 'MagicStick', path: '/console/activation' },
|
||||||
{ name: 'channel', label: '渠道商管理', icon: 'Setting', path: '/console/channel' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 根据权限动态添加渠道商管理
|
||||||
|
const navItems = hasPermission
|
||||||
|
? [...baseNavItems, { name: 'channel', label: '渠道商管理', icon: 'Setting', path: '/console/channel' }]
|
||||||
|
: baseNavItems;
|
||||||
|
|
||||||
// 当前激活的菜单
|
// 当前激活的菜单
|
||||||
const activeNav = computed(() => {
|
const activeNav = computed(() => {
|
||||||
const path = route.path;
|
const path = route.path;
|
||||||
@@ -97,13 +115,13 @@ window.addEventListener('resize', checkIsMobile);
|
|||||||
<div class="content-main">
|
<div class="content-main">
|
||||||
<div v-if="isMobile" class="content-header">
|
<div v-if="isMobile" class="content-header">
|
||||||
<div class="mobile-toggle" @click="isCollapsed = false">
|
<div class="mobile-toggle" @click="isCollapsed = false">
|
||||||
<el-icon><i-ep-expand /></el-icon>
|
<el-icon><Expand /></el-icon>
|
||||||
<span>菜单</span>
|
<span>控制台</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component :is="Component" />
|
<component :is="Component" :external-invite-code="inviteCodeFromUrl" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,7 +242,7 @@ window.addEventListener('resize', checkIsMobile);
|
|||||||
|
|
||||||
.content-main {
|
.content-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
transition: margin-left 0.3s ease;
|
transition: margin-left 0.3s ease;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ModelApiTypeOption, ModelLibraryDto, ModelTypeOption } from '@/api/model/types';
|
import type { ModelApiTypeOption, ModelLibraryDto, ModelTypeOption } from '@/api/model/types';
|
||||||
import { Box, Close, CopyDocument, HomeFilled, OfficeBuilding, Search } from '@element-plus/icons-vue';
|
import { Box, Close, CopyDocument, HomeFilled, OfficeBuilding, Search, Monitor } from '@element-plus/icons-vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
@@ -33,6 +33,10 @@ const modelTypeOptions = ref<ModelTypeOption[]>([]);
|
|||||||
// API类型选项
|
// API类型选项
|
||||||
const apiTypeOptions = ref<ModelApiTypeOption[]>([]);
|
const apiTypeOptions = ref<ModelApiTypeOption[]>([]);
|
||||||
|
|
||||||
|
function goToMonitor() {
|
||||||
|
window.open('http://data.ccnetcore.com:91/?period=24h', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchModelList() {
|
async function fetchModelList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -265,15 +269,20 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-button
|
<div class="monitor-card" @click="goToMonitor">
|
||||||
v-if="false"
|
<div class="monitor-icon">
|
||||||
:icon="HomeFilled"
|
<el-icon class="ecg-icon"><Monitor /></el-icon>
|
||||||
class="home-btn"
|
</div>
|
||||||
round
|
<div class="monitor-info">
|
||||||
@click="goToHome"
|
<div class="monitor-value">
|
||||||
>
|
实时监控
|
||||||
返回首页
|
<span class="live-dot" />
|
||||||
</el-button>
|
</div>
|
||||||
|
<div class="monitor-label">
|
||||||
|
服务可用性矩阵
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -662,20 +671,66 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-btn {
|
.monitor-card {
|
||||||
background: rgba(255, 255, 255, 0.25);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
||||||
color: white;
|
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
padding: 10px 24px;
|
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||||
font-weight: 500;
|
border-radius: 12px;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
padding: 12px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.35);
|
background: rgba(255, 255, 255, 0.25);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
.ecg-icon {
|
||||||
|
animation: heartbeat 1.5s ease-in-out infinite both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-info {
|
||||||
|
.monitor-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.live-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #67c23a;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 8px #67c23a;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1161,7 +1216,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-btn {
|
.monitor-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1380,4 +1435,18 @@ onMounted(() => {
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes heartbeat {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
14% { transform: scale(1.3); }
|
||||||
|
28% { transform: scale(1); }
|
||||||
|
42% { transform: scale(1.3); }
|
||||||
|
70% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.7); }
|
||||||
|
70% { opacity: 0.7; box-shadow: 0 0 0 6px rgba(103, 194, 58, 0); }
|
||||||
|
100% { opacity: 1; box-shadow: 0 0 0 0 rgba(103, 194, 58, 0); }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// 页面效果查看
|
||||||
|
// https://ai.ccnetcore.com/pay-result?charset=UTF-8&out_trade_no=YI_20251015232040_3316&method=alipay.trade.page.pay.return&total_amount=215.90&sign=htfny1D%2B8wcLZzK7StevZG%2BD441RLXvksoAR%2BzOq%2B1WHMwfJdVkzyZF2bBmvbU%2FsHBB2HMl8TT3KoaHaf8UfWZgtDGbMoQC%2F1O%2BRcEw8jljlpW3XLMdKGx6dytqZkhq9lRD6tR3ofBiuviv2PmxVd1l%2Bcqs7nwNWwKJWonWI0c5UOE%2BYWgg3hjEJnMYVQjUb6FvrVLfANEU0YyTO%2Bi6vL55Gwug6GIXvGqUPZc3GbwXc%2FUHnu1qv4Yi6tc1dtUoLUNHVfTKrC2N55T84AALZteIK0m7suzrkvBPcKdpn4NGVDtv5cCBCHPjtD3COrNISrNUf3sQXpTvqJGw6dWag6g%3D%3D&trade_no=2025101522001438971445003566&auth_app_id=2021005182687851&version=1.0&app_id=2021005182687851&sign_type=RSA2&seller_id=2088870286599802×tamp=2025-10-15+23%3A21%3A01
|
||||||
|
|
||||||
import { ElButton, ElDivider, ElMessage } from 'element-plus';
|
import { ElButton, ElDivider, ElMessage } from 'element-plus';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
|
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
import { useNProgress } from '@vueuse/integrations/useNProgress';
|
import { useNProgress } from '@vueuse/integrations/useNProgress';
|
||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import { ROUTER_WHITE_LIST } from '@/config';
|
import { ROUTER_WHITE_LIST } from '@/config';
|
||||||
|
import { checkPagePermission } from '@/config/permission';
|
||||||
import { errorRouter, layoutRouter, staticRouter } from '@/routers/modules/staticRouter';
|
import { errorRouter, layoutRouter, staticRouter } from '@/routers/modules/staticRouter';
|
||||||
import { useDesignStore, useUserStore } from '@/stores';
|
import { useDesignStore, useUserStore } from '@/stores';
|
||||||
|
|
||||||
@@ -63,6 +65,16 @@ router.beforeEach(
|
|||||||
if (!userStore.token)
|
if (!userStore.token)
|
||||||
userStore.logout();
|
userStore.logout();
|
||||||
|
|
||||||
|
// 7. 页面权限检查
|
||||||
|
const userName = userStore.userInfo?.user?.userName;
|
||||||
|
const hasPermission = checkPagePermission(to.path, userName);
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
// 用户无权访问该页面,跳转到403页面
|
||||||
|
ElMessage.warning('您没有权限访问该页面');
|
||||||
|
return next('/403');
|
||||||
|
}
|
||||||
|
|
||||||
// 其余逻辑 预留...
|
// 其余逻辑 预留...
|
||||||
|
|
||||||
// 8. 放行路由
|
// 8. 放行路由
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'chat',
|
name: 'chat',
|
||||||
component: () => import('@/pages/chat/index.vue'),
|
component: () => import('@/pages/chat/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'AI聊天',
|
title: 'AI应用',
|
||||||
icon: 'HomeFilled',
|
icon: 'HomeFilled',
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
@@ -32,7 +32,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'chatConversation',
|
name: 'chatConversation',
|
||||||
component: () => import('@/pages/chat/conversation/index.vue'),
|
component: () => import('@/pages/chat/conversation/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'AI对话',
|
title: '意心Ai-AI对话',
|
||||||
isDefaultChat: true,
|
isDefaultChat: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -41,7 +41,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'chatConversationWithId',
|
name: 'chatConversationWithId',
|
||||||
component: () => import('@/pages/chat/conversation/index.vue'),
|
component: () => import('@/pages/chat/conversation/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'AI对话',
|
title: '意心Ai-AI对话',
|
||||||
isDefaultChat: false,
|
isDefaultChat: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -50,7 +50,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'chatImage',
|
name: 'chatImage',
|
||||||
component: () => import('@/pages/chat/image/index.vue'),
|
component: () => import('@/pages/chat/image/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '图片生成',
|
title: '意心Ai-AI图片',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -58,7 +58,15 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'chatVideo',
|
name: 'chatVideo',
|
||||||
component: () => import('@/pages/chat/video/index.vue'),
|
component: () => import('@/pages/chat/video/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '视频生成',
|
title: '意心Ai-AI视频',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'agent',
|
||||||
|
name: 'monitor',
|
||||||
|
component: () => import('@/pages/chat/agent/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '意心Ai-AI智能体',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -70,7 +78,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'products',
|
name: 'products',
|
||||||
component: () => import('@/pages/products/index.vue'),
|
component: () => import('@/pages/products/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '产品页面',
|
title: '意心Ai-产品页面',
|
||||||
keepAlive: 0,
|
keepAlive: 0,
|
||||||
isDefaultChat: false,
|
isDefaultChat: false,
|
||||||
layout: 'blankPage',
|
layout: 'blankPage',
|
||||||
@@ -83,7 +91,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'modelLibrary',
|
name: 'modelLibrary',
|
||||||
component: () => import('@/pages/modelLibrary/index.vue'),
|
component: () => import('@/pages/modelLibrary/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '模型库',
|
title: '意心Ai-模型库',
|
||||||
keepAlive: 0,
|
keepAlive: 0,
|
||||||
isDefaultChat: false,
|
isDefaultChat: false,
|
||||||
layout: 'default',
|
layout: 'default',
|
||||||
@@ -96,7 +104,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'payResult',
|
name: 'payResult',
|
||||||
component: () => import('@/pages/payResult/index.vue'),
|
component: () => import('@/pages/payResult/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '支付结果',
|
title: '意心Ai-支付结果',
|
||||||
keepAlive: 0,
|
keepAlive: 0,
|
||||||
isDefaultChat: false,
|
isDefaultChat: false,
|
||||||
layout: 'blankPage',
|
layout: 'blankPage',
|
||||||
@@ -109,7 +117,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'activityDetail',
|
name: 'activityDetail',
|
||||||
component: () => import('@/pages/activity/detail.vue'),
|
component: () => import('@/pages/activity/detail.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '活动详情',
|
title: '意心Ai-活动详情',
|
||||||
isDefaultChat: false,
|
isDefaultChat: false,
|
||||||
layout: 'blankPage',
|
layout: 'blankPage',
|
||||||
},
|
},
|
||||||
@@ -121,7 +129,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'announcementDetail',
|
name: 'announcementDetail',
|
||||||
component: () => import('@/pages/announcement/detail.vue'),
|
component: () => import('@/pages/announcement/detail.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '公告详情',
|
title: '意心Ai-公告详情',
|
||||||
isDefaultChat: false,
|
isDefaultChat: false,
|
||||||
layout: 'blankPage',
|
layout: 'blankPage',
|
||||||
},
|
},
|
||||||
@@ -133,7 +141,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'console',
|
name: 'console',
|
||||||
component: () => import('@/pages/console/index.vue'),
|
component: () => import('@/pages/console/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '控制台',
|
title: '意心Ai-控制台',
|
||||||
icon: 'Setting',
|
icon: 'Setting',
|
||||||
layout: 'default',
|
layout: 'default',
|
||||||
},
|
},
|
||||||
@@ -148,7 +156,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'consoleUser',
|
name: 'consoleUser',
|
||||||
component: () => import('@/components/userPersonalCenter/components/UserManagement.vue'),
|
component: () => import('@/components/userPersonalCenter/components/UserManagement.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '用户信息',
|
title: '意心Ai-用户信息',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -156,7 +164,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'consoleApikey',
|
name: 'consoleApikey',
|
||||||
component: () => import('@/components/userPersonalCenter/components/APIKeyManagement.vue'),
|
component: () => import('@/components/userPersonalCenter/components/APIKeyManagement.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'API密钥',
|
title: '意心Ai-API密钥',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -164,7 +172,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'consoleRechargeLog',
|
name: 'consoleRechargeLog',
|
||||||
component: () => import('@/components/userPersonalCenter/components/RechargeLog.vue'),
|
component: () => import('@/components/userPersonalCenter/components/RechargeLog.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '充值记录',
|
title: '意心Ai-充值记录',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -172,7 +180,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'consoleUsage',
|
name: 'consoleUsage',
|
||||||
component: () => import('@/components/userPersonalCenter/components/UsageStatistics.vue'),
|
component: () => import('@/components/userPersonalCenter/components/UsageStatistics.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '用量统计',
|
title: '意心Ai-用量统计',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -180,7 +188,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'consolePremium',
|
name: 'consolePremium',
|
||||||
component: () => import('@/components/userPersonalCenter/components/PremiumService.vue'),
|
component: () => import('@/components/userPersonalCenter/components/PremiumService.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '尊享服务',
|
title: '意心Ai-尊享服务',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -188,7 +196,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'consoleDailyTask',
|
name: 'consoleDailyTask',
|
||||||
component: () => import('@/components/userPersonalCenter/components/DailyTask.vue'),
|
component: () => import('@/components/userPersonalCenter/components/DailyTask.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '每日任务',
|
title: '意心Ai-每日任务',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -196,7 +204,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'consoleInvite',
|
name: 'consoleInvite',
|
||||||
component: () => import('@/components/userPersonalCenter/components/CardFlipActivity.vue'),
|
component: () => import('@/components/userPersonalCenter/components/CardFlipActivity.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '每周邀请',
|
title: '意心Ai-每周邀请',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -204,7 +212,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'consoleActivation',
|
name: 'consoleActivation',
|
||||||
component: () => import('@/components/userPersonalCenter/components/ActivationCode.vue'),
|
component: () => import('@/components/userPersonalCenter/components/ActivationCode.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '激活码兑换',
|
title: '意心Ai-激活码兑换',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -212,7 +220,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
name: 'consoleChannel',
|
name: 'consoleChannel',
|
||||||
component: () => import('@/pages/console/channel/index.vue'),
|
component: () => import('@/pages/console/channel/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '渠道商管理',
|
title: '意心Ai-渠道商管理',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -193,16 +193,16 @@
|
|||||||
--el-color-success: var(--color-success) !important;
|
--el-color-success: var(--color-success) !important;
|
||||||
--el-color-warning: var(--color-warning) !important;
|
--el-color-warning: var(--color-warning) !important;
|
||||||
--el-color-danger: var(--color-danger) !important;
|
--el-color-danger: var(--color-danger) !important;
|
||||||
--el-color-info: var(--color-info) !important;
|
//--el-color-info: var(--color-info) !important;
|
||||||
--el-font-size-base: var(--font-size-base) !important;
|
--el-font-size-base: var(--font-size-base) !important;
|
||||||
--el-font-family: var(--font-family-sans) !important;
|
--el-font-family: var(--font-family-sans) !important;
|
||||||
|
|
||||||
/* Element Plus 组件特定变量 */
|
/* Element Plus 组件特定变量 */
|
||||||
--el-menu-item-height: 48px;
|
--el-menu-item-height: 48px;
|
||||||
--el-menu-bg-color: var(--sidebar-background-color);
|
//--el-menu-bg-color: var(--sidebar-background-color);
|
||||||
--el-menu-text-color: var(--text-color-secondary);
|
//--el-menu-text-color: var(--text-color-secondary);
|
||||||
--el-menu-active-color: var(--color-primary);
|
--el-menu-active-color: var(--color-primary);
|
||||||
--el-menu-hover-bg-color: var(--color-gray-100);
|
//--el-menu-hover-bg-color: var(--color-gray-100);
|
||||||
|
|
||||||
/* 表单相关 */
|
/* 表单相关 */
|
||||||
--el-form-label-font-size: var(--font-size-sm);
|
--el-form-label-font-size: var(--font-size-sm);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
import { createApp, h } from 'vue';
|
import { createApp, h } from 'vue';
|
||||||
import ProductPackage from '@/components/ProductPackage/index.vue';
|
import ProductPackage from '@/components/ProductPackage/index.vue';
|
||||||
|
import router from '@/routers';
|
||||||
|
import store from '@/stores';
|
||||||
|
|
||||||
export function showProductPackage() {
|
export function showProductPackage() {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@@ -16,5 +20,16 @@ export function showProductPackage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 关键:必须在 mount 之前按顺序注册所有依赖
|
||||||
|
app.use(store); // 1. 先注册 store
|
||||||
|
app.use(router); // 2. 再注册 router
|
||||||
|
app.use(ElementPlus); // 3. 最后注册 ElementPlus
|
||||||
|
|
||||||
|
// 注册 Element Plus 图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后才挂载应用
|
||||||
app.mount(div);
|
app.mount(div);
|
||||||
}
|
}
|
||||||
|
|||||||
4
Yi.Ai.Vue3/types/components.d.ts
vendored
4
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -19,6 +19,7 @@ declare module 'vue' {
|
|||||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCheckTag: typeof import('element-plus/es')['ElCheckTag']
|
ElCheckTag: typeof import('element-plus/es')['ElCheckTag']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
@@ -36,6 +37,7 @@ declare module 'vue' {
|
|||||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElImage: typeof import('element-plus/es')['ElImage']
|
ElImage: typeof import('element-plus/es')['ElImage']
|
||||||
|
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElMain: typeof import('element-plus/es')['ElMain']
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
@@ -59,6 +61,7 @@ declare module 'vue' {
|
|||||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
|
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
|
||||||
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
||||||
Indexl: typeof import('./../src/components/SupportModelProducts/indexl.vue')['default']
|
Indexl: typeof import('./../src/components/SupportModelProducts/indexl.vue')['default']
|
||||||
@@ -87,6 +90,7 @@ declare module 'vue' {
|
|||||||
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
|
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
|
||||||
}
|
}
|
||||||
export interface GlobalDirectives {
|
export interface GlobalDirectives {
|
||||||
|
vInfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll']
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user