feat: ai完成stock模块搭建
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Yi.Framework.Stock.Application.Contracts.Dtos.StockMarket;
|
||||||
|
/// <summary>
|
||||||
|
/// 创建股市输入DTO
|
||||||
|
/// </summary>
|
||||||
|
public class CreateStockMarketInputDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 股市代码
|
||||||
|
/// </summary>
|
||||||
|
public string MarketCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 股市名称
|
||||||
|
/// </summary>
|
||||||
|
public string MarketName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 股市描述
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
}
|
||||||
@@ -38,5 +38,10 @@ namespace Yi.Framework.Stock.Application.Contracts.Dtos.StockPrice
|
|||||||
/// 时间周期类型
|
/// 时间周期类型
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PeriodTypeEnum PeriodType { get; set; }
|
public PeriodTypeEnum PeriodType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 记录时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime RecordTime { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,12 +12,12 @@ namespace Yi.Framework.Stock.Application.Contracts.Dtos.StockTransaction
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 股票代码
|
/// 股票代码
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string StockCode { get; set; }
|
public string? StockCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 股票名称
|
/// 股票名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string StockName { get; set; }
|
public string? StockName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 交易类型
|
/// 交易类型
|
||||||
|
|||||||
@@ -39,5 +39,11 @@ namespace Yi.Framework.Stock.Application.Contracts.IServices
|
|||||||
/// <param name="input">卖出股票参数</param>
|
/// <param name="input">卖出股票参数</param>
|
||||||
/// <returns>操作结果</returns>
|
/// <returns>操作结果</returns>
|
||||||
Task SellStockAsync(SellStockInputDto input);
|
Task SellStockAsync(SellStockInputDto input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成最新股票记录
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>操作结果</returns>
|
||||||
|
Task GenerateStocksAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
<PackageReference Include="Volo.Abp.SettingManagement.Application.Contracts" Version="$(AbpVersion)" />
|
<PackageReference Include="Volo.Abp.SettingManagement.Application.Contracts" Version="$(AbpVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Dtos\" />
|
|
||||||
<Folder Include="IServices\" />
|
<Folder Include="IServices\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using Yi.Framework.Stock.Application.Contracts.IServices;
|
|||||||
using Yi.Framework.Stock.Domain.Entities;
|
using Yi.Framework.Stock.Domain.Entities;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
using Yi.Framework.Stock.Domain.Managers;
|
using Yi.Framework.Stock.Domain.Managers;
|
||||||
|
using Mapster;
|
||||||
|
|
||||||
namespace Yi.Framework.Stock.Application.Services
|
namespace Yi.Framework.Stock.Application.Services
|
||||||
{
|
{
|
||||||
@@ -34,6 +35,23 @@ namespace Yi.Framework.Stock.Application.Services
|
|||||||
_stockMarketManager = stockMarketManager;
|
_stockMarketManager = stockMarketManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建股市
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("stock/markets")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<StockMarketDto> CreateStockMarketAsync(CreateStockMarketInputDto input)
|
||||||
|
{
|
||||||
|
// 使用映射将输入DTO转换为实体
|
||||||
|
var stockMarket = input.Adapt<StockMarketAggregateRoot>();
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
var result = await _stockMarketRepository.InsertReturnEntityAsync(stockMarket);
|
||||||
|
|
||||||
|
// 使用映射将实体转换为返回DTO
|
||||||
|
return result.Adapt<StockMarketDto>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取股市列表
|
/// 获取股市列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -87,6 +105,7 @@ namespace Yi.Framework.Stock.Application.Services
|
|||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
StockId = p.StockId,
|
StockId = p.StockId,
|
||||||
CreationTime = p.CreationTime,
|
CreationTime = p.CreationTime,
|
||||||
|
RecordTime = p.RecordTime,
|
||||||
CurrentPrice = p.CurrentPrice,
|
CurrentPrice = p.CurrentPrice,
|
||||||
Volume = p.Volume,
|
Volume = p.Volume,
|
||||||
Turnover = p.Turnover,
|
Turnover = p.Turnover,
|
||||||
@@ -132,5 +151,16 @@ namespace Yi.Framework.Stock.Application.Services
|
|||||||
input.Quantity
|
input.Quantity
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成最新股票记录
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("stock/generate")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task GenerateStocksAsync()
|
||||||
|
{
|
||||||
|
await _stockMarketManager.GenerateStocksAsync();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ using Yi.Framework.Stock.Application.Contracts.Dtos.StockNews;
|
|||||||
using Yi.Framework.Stock.Application.Contracts.IServices;
|
using Yi.Framework.Stock.Application.Contracts.IServices;
|
||||||
using Yi.Framework.Stock.Domain.Entities;
|
using Yi.Framework.Stock.Domain.Entities;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
using Yi.Framework.Stock.Domain.Managers;
|
||||||
|
|
||||||
namespace Yi.Framework.Stock.Application.Services
|
namespace Yi.Framework.Stock.Application.Services
|
||||||
{
|
{
|
||||||
@@ -17,16 +18,20 @@ namespace Yi.Framework.Stock.Application.Services
|
|||||||
public class StockNewsService : ApplicationService, IStockNewsService
|
public class StockNewsService : ApplicationService, IStockNewsService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarRepository<StockNewsAggregateRoot> _stockNewsRepository;
|
private readonly ISqlSugarRepository<StockNewsAggregateRoot> _stockNewsRepository;
|
||||||
|
private readonly NewsManager _newsManager;
|
||||||
|
|
||||||
public StockNewsService(ISqlSugarRepository<StockNewsAggregateRoot> stockNewsRepository)
|
public StockNewsService(
|
||||||
|
ISqlSugarRepository<StockNewsAggregateRoot> stockNewsRepository,
|
||||||
|
NewsManager newsManager)
|
||||||
{
|
{
|
||||||
_stockNewsRepository = stockNewsRepository;
|
_stockNewsRepository = stockNewsRepository;
|
||||||
|
_newsManager = newsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取股市新闻列表
|
/// 获取股市新闻列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("/api/stock/news")]
|
[HttpGet("/api/app/stock/news")]
|
||||||
public async Task<PagedResultDto<StockNewsDto>> GetStockNewsListAsync(StockNewsGetListInputDto input)
|
public async Task<PagedResultDto<StockNewsDto>> GetStockNewsListAsync(StockNewsGetListInputDto input)
|
||||||
{
|
{
|
||||||
RefAsync<int> total = 0;
|
RefAsync<int> total = 0;
|
||||||
@@ -61,5 +66,15 @@ namespace Yi.Framework.Stock.Application.Services
|
|||||||
|
|
||||||
return new PagedResultDto<StockNewsDto>(total, list);
|
return new PagedResultDto<StockNewsDto>(total, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成股市新闻
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>生成结果</returns>
|
||||||
|
[HttpPost("/api/app/stock/news/generate")]
|
||||||
|
public async Task GenerateNewsAsync()
|
||||||
|
{
|
||||||
|
await _newsManager.GenerateNewsAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ namespace Yi.Framework.Stock.Domain.Entities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建时间
|
/// 创建时间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime CreationTime { get; set; } = DateTime.Now;
|
public DateTime CreationTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建者
|
/// 创建者
|
||||||
@@ -52,13 +52,11 @@ namespace Yi.Framework.Stock.Domain.Entities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 股市代码
|
/// 股市代码
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>如:SH、SZ、HK等</remarks>
|
|
||||||
public string MarketCode { get; set; } = string.Empty;
|
public string MarketCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 股市名称
|
/// 股市名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>如:上海证券交易所、深圳证券交易所等</remarks>
|
|
||||||
public string MarketName { get; set; } = string.Empty;
|
public string MarketName { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -21,11 +21,16 @@ namespace Yi.Framework.Stock.Domain.Entities
|
|||||||
/// <remarks>关联到具体的股票</remarks>
|
/// <remarks>关联到具体的股票</remarks>
|
||||||
public Guid StockId { get; set; }
|
public Guid StockId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间(审计日志)
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreationTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 记录时间
|
/// 记录时间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>价格记录的时间点</remarks>
|
/// <remarks>价格记录的实际时间点</remarks>
|
||||||
public DateTime CreationTime { get; set; }
|
public DateTime RecordTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 当前价
|
/// 当前价
|
||||||
@@ -63,6 +68,7 @@ namespace Yi.Framework.Stock.Domain.Entities
|
|||||||
{
|
{
|
||||||
StockId = stockId;
|
StockId = stockId;
|
||||||
CreationTime = DateTime.Now;
|
CreationTime = DateTime.Now;
|
||||||
|
RecordTime = DateTime.Now;
|
||||||
CurrentPrice = currentPrice;
|
CurrentPrice = currentPrice;
|
||||||
Volume = volume;
|
Volume = volume;
|
||||||
Turnover = turnover;
|
Turnover = turnover;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using Yi.Framework.Stock.Domain.Managers.Plugins;
|
using Volo.Abp.Domain.Services;
|
||||||
using Volo.Abp.Domain.Services;
|
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
using Yi.Framework.Stock.Domain.Entities;
|
using Yi.Framework.Stock.Domain.Entities;
|
||||||
|
using Yi.Framework.Stock.Domain.Managers.SemanticKernel;
|
||||||
|
using Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins;
|
||||||
|
|
||||||
namespace Yi.Framework.Stock.Domain.Managers;
|
namespace Yi.Framework.Stock.Domain.Managers;
|
||||||
|
|
||||||
public class NewsManager:DomainService
|
public class NewsManager:DomainService
|
||||||
@@ -19,9 +21,12 @@ public class NewsManager:DomainService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task GenerateNewsAsync()
|
public async Task GenerateNewsAsync()
|
||||||
{
|
{ var question = """
|
||||||
_skClient.RegisterPlugins<NewsPlugins>("news");
|
生成并保存一个新闻,包含新闻标题、新闻内容、新闻简介、新闻来源
|
||||||
await _skClient.ChatCompletionAsync("帮我生成一个新闻");
|
内容关于娱乐圈
|
||||||
|
只用生成一次即可
|
||||||
|
""";
|
||||||
|
await _skClient.ChatCompletionAsync(question, ("NewsPlugins","save_news"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveNewsAsync(NewsModel news)
|
public async Task SaveNewsAsync(NewsModel news)
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.SemanticKernel;
|
using Microsoft.SemanticKernel;
|
||||||
using Yi.Framework.Stock.Domain.Managers;
|
|
||||||
|
|
||||||
namespace Yi.Framework.Stock.Domain.Managers.Plugins;
|
namespace Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins;
|
||||||
|
|
||||||
public class NewsPlugins
|
public class NewsPlugins
|
||||||
{
|
{
|
||||||
private readonly NewsManager _newsManager;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
public NewsPlugins(NewsManager newsManager)
|
public NewsPlugins(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
_newsManager = newsManager;
|
_serviceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
[KernelFunction("save_news"), Description("生成并且保存一个新闻")]
|
[KernelFunction("save_news"), Description("生成并保存一个新闻")]
|
||||||
public async Task SaveAsync(NewsModel news)
|
public async Task SaveAsync(NewsModel news)
|
||||||
{
|
{
|
||||||
await _newsManager.SaveNewsAsync(news);
|
var newsManager = _serviceProvider.GetRequiredService<NewsManager>();
|
||||||
|
await newsManager.SaveNewsAsync(news);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,15 +30,13 @@ public class NewsModel
|
|||||||
|
|
||||||
[JsonPropertyName("content")]
|
[JsonPropertyName("content")]
|
||||||
[DisplayName("新闻内容")]
|
[DisplayName("新闻内容")]
|
||||||
public string? Content { get; set; }
|
public string Content { get; set; }
|
||||||
|
|
||||||
//新闻简介
|
|
||||||
[JsonPropertyName("summary")]
|
[JsonPropertyName("summary")]
|
||||||
[DisplayName("新闻简介")]
|
[DisplayName("新闻简介")]
|
||||||
public string? Summary { get; set; }
|
public string Summary { get; set; }
|
||||||
|
|
||||||
//新闻来源
|
|
||||||
[JsonPropertyName("source")]
|
[JsonPropertyName("source")]
|
||||||
[DisplayName("新闻来源")]
|
[DisplayName("新闻来源")]
|
||||||
public string? Source { get; set; }
|
public string Source { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,34 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.SemanticKernel;
|
using Microsoft.SemanticKernel;
|
||||||
|
|
||||||
namespace Yi.Framework.Stock.Domain.Managers.Plugins;
|
namespace Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins;
|
||||||
|
|
||||||
public class StockPlugins
|
public class StockPlugins
|
||||||
{
|
{
|
||||||
[KernelFunction("save_stocks"), Description("生成并且保存多个股票记录")]
|
private readonly IServiceProvider _serviceProvider;
|
||||||
public async Task<string> SaveAsync(List<StockModel> stockModels)
|
|
||||||
|
public StockPlugins(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
return "成功";
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
[KernelFunction("save_stocks"), Description("生成并且保存多个股票记录")]
|
||||||
|
public async Task SaveAsync(List<StockModel> stockModels)
|
||||||
|
{
|
||||||
|
var stockMarketManager= _serviceProvider.GetRequiredService<StockMarketManager>();
|
||||||
|
await stockMarketManager.SaveStockAsync(stockModels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StockModel
|
public class StockModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")]
|
||||||
public int Id { get; set; }
|
[DisplayName("股票id")]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
[JsonPropertyName("values")]
|
||||||
public string Name { get; set; }
|
[DisplayName("股票未来24小时价格")]
|
||||||
|
public decimal[] Values { get; set; }
|
||||||
[JsonPropertyName("is_on")]
|
|
||||||
public bool? IsOn { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -1,57 +1,18 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.SemanticKernel;
|
||||||
using Microsoft.SemanticKernel;
|
|
||||||
using Microsoft.SemanticKernel.ChatCompletion;
|
using Microsoft.SemanticKernel.ChatCompletion;
|
||||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
||||||
namespace Yi.Framework.Stock.Domain.Managers;
|
namespace Yi.Framework.Stock.Domain.Managers.SemanticKernel;
|
||||||
|
|
||||||
public class SemanticKernelClient:ITransientDependency
|
public class SemanticKernelClient:ITransientDependency
|
||||||
{
|
{
|
||||||
private Kernel Kernel { get; }
|
public Kernel Kernel { get;}
|
||||||
private readonly IKernelBuilder _kernelBuilder;
|
|
||||||
private SemanticKernelOptions Options { get; }
|
|
||||||
|
|
||||||
public SemanticKernelClient(IOptions<SemanticKernelOptions> semanticKernelOption)
|
public SemanticKernelClient(Kernel kernel)
|
||||||
{
|
{
|
||||||
Options = semanticKernelOption.Value;
|
this.Kernel = kernel;
|
||||||
_kernelBuilder = Kernel.CreateBuilder();
|
|
||||||
RegisterChatCompletion();
|
|
||||||
Kernel = _kernelBuilder.Build();
|
|
||||||
RegisterDefautlPlugins();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注册
|
|
||||||
/// </summary>
|
|
||||||
private void RegisterChatCompletion()
|
|
||||||
{
|
|
||||||
_kernelBuilder.AddOpenAIChatCompletion(
|
|
||||||
modelId: Options.ModelId,
|
|
||||||
apiKey: Options.ApiKey,
|
|
||||||
httpClient: new HttpClient() { BaseAddress = new Uri(Options.Endpoint) });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 插件注册
|
|
||||||
/// </summary>
|
|
||||||
private void RegisterDefautlPlugins()
|
|
||||||
{
|
|
||||||
//动态导入插件
|
|
||||||
// this.Kernel.ImportPluginFromPromptDirectory(System.IO.Path.Combine("wwwroot", "plugin","stock"),"stock");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 自定义插件
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="pluginName"></param>
|
|
||||||
/// <typeparam name="TPlugin"></typeparam>
|
|
||||||
public void RegisterPlugins<TPlugin>(string pluginName)
|
|
||||||
{
|
|
||||||
this.Kernel.Plugins.AddFromType<TPlugin>(pluginName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 执行插件
|
/// 执行插件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -70,19 +31,24 @@ public class SemanticKernelClient:ITransientDependency
|
|||||||
/// 聊天对话,调用方法
|
/// 聊天对话,调用方法
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IReadOnlyList<ChatMessageContent>> ChatCompletionAsync(string question)
|
public async Task<IReadOnlyList<ChatMessageContent>> ChatCompletionAsync(string question,params (string,string)[] functions)
|
||||||
{
|
{
|
||||||
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
|
if (functions is null)
|
||||||
{
|
{
|
||||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
throw new Exception("请选择插件");
|
||||||
MaxTokens = Options.MaxTokens
|
}
|
||||||
|
var openSettings = new OpenAIPromptExecutionSettings()
|
||||||
|
{
|
||||||
|
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions.Select(x=>this.Kernel.Plugins.GetFunction(x.Item1, x.Item2)).ToList(),true),
|
||||||
|
// ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
|
||||||
|
MaxTokens =1000
|
||||||
};
|
};
|
||||||
|
|
||||||
var chatCompletionService = this.Kernel.GetRequiredService<IChatCompletionService>();
|
var chatCompletionService = this.Kernel.GetRequiredService<IChatCompletionService>();
|
||||||
|
|
||||||
var results =await chatCompletionService.GetChatMessageContentsAsync(
|
var results =await chatCompletionService.GetChatMessageContentsAsync(
|
||||||
question,
|
question,
|
||||||
executionSettings: openAIPromptExecutionSettings,
|
executionSettings: openSettings,
|
||||||
kernel: Kernel);
|
kernel: Kernel);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,9 @@
|
|||||||
namespace Yi.Framework.Stock.Domain.Managers;
|
namespace Yi.Framework.Stock.Domain.Managers.SemanticKernel
|
||||||
|
|
||||||
public class SemanticKernelOptions
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class SemanticKernelOptions
|
||||||
/// OpenAI 模型 ID
|
{
|
||||||
/// </summary>
|
public string ModelId { get; set; }
|
||||||
public string ModelId { get; set; } = string.Empty;
|
public string Endpoint { get; set; }
|
||||||
|
public string ApiKey { get; set; }
|
||||||
/// <summary>
|
}
|
||||||
/// OpenAI API 密钥
|
}
|
||||||
/// </summary>
|
|
||||||
public string ApiKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// API 端点地址
|
|
||||||
/// </summary>
|
|
||||||
public string Endpoint { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 最大生成令牌数
|
|
||||||
/// </summary>
|
|
||||||
public int MaxTokens { get; set; } = 1000;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 插件目录路径
|
|
||||||
/// </summary>
|
|
||||||
public string PluginsDirectoryPath { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,8 @@ using Yi.Framework.Stock.Domain.Entities;
|
|||||||
using Yi.Framework.Stock.Domain.Shared;
|
using Yi.Framework.Stock.Domain.Shared;
|
||||||
using Yi.Framework.Stock.Domain.Shared.Etos;
|
using Yi.Framework.Stock.Domain.Shared.Etos;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
using Yi.Framework.Stock.Domain.Managers.Plugins;
|
using Yi.Framework.Stock.Domain.Managers.SemanticKernel;
|
||||||
|
using Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins;
|
||||||
|
|
||||||
namespace Yi.Framework.Stock.Domain.Managers
|
namespace Yi.Framework.Stock.Domain.Managers
|
||||||
{
|
{
|
||||||
@@ -277,27 +278,25 @@ namespace Yi.Framework.Stock.Domain.Managers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="priceRecords">价格记录列表</param>
|
/// <param name="priceRecords">价格记录列表</param>
|
||||||
/// <returns>保存的记录数量</returns>
|
/// <returns>保存的记录数量</returns>
|
||||||
public async Task<int> BatchSaveStockPriceRecordsAsync(List<StockPriceRecordEntity> priceRecords)
|
public async Task BatchSaveStockPriceRecordsAsync(List<StockPriceRecordEntity> priceRecords)
|
||||||
{
|
{
|
||||||
if (priceRecords == null || !priceRecords.Any())
|
if (priceRecords == null || !priceRecords.Any())
|
||||||
{
|
{
|
||||||
return 0;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证数据
|
// 验证数据
|
||||||
foreach (var record in priceRecords)
|
for (int i = 0; i < priceRecords.Count; i++)
|
||||||
{
|
{
|
||||||
|
var record = priceRecords[i];
|
||||||
if (record.CurrentPrice <= 0)
|
if (record.CurrentPrice <= 0)
|
||||||
{
|
{
|
||||||
throw new UserFriendlyException($"股票ID {record.StockId} 的价格必须大于0");
|
throw new UserFriendlyException($"股票ID {record.StockId} 的价格必须大于0");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置创建时间为当前时间(如果未设置)
|
// 设置记录时间(当前时间加上i个小时),只记录到年月日小时
|
||||||
if (record.CreationTime == default)
|
record.RecordTime = new DateTime(DateTime.Now.AddHours(i).Year, DateTime.Now.AddHours(i).Month, DateTime.Now.AddHours(i).Day, DateTime.Now.AddHours(i).Hour, 0, 0);
|
||||||
{
|
|
||||||
record.CreationTime = DateTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算交易额(如果未设置)
|
// 计算交易额(如果未设置)
|
||||||
if (record.Turnover == 0 && record.Volume > 0)
|
if (record.Turnover == 0 && record.Volume > 0)
|
||||||
{
|
{
|
||||||
@@ -306,18 +305,85 @@ namespace Yi.Framework.Stock.Domain.Managers
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _stockPriceRecordRepository.InsertManyAsync(priceRecords);
|
await _stockPriceRecordRepository.InsertManyAsync(priceRecords);
|
||||||
return priceRecords.Count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SaveStockAsync(List<StockModel> stockModels)
|
||||||
|
{
|
||||||
|
if (stockModels == null || !stockModels.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有股票ID
|
||||||
|
var stockIds = stockModels.Select(m => m.Id).ToList();
|
||||||
|
|
||||||
|
// 一次性查询所有相关股票信息
|
||||||
|
var stockMarkets = await _stockMarketRepository.GetListAsync(s => stockIds.Contains(s.Id));
|
||||||
|
|
||||||
|
// 构建字典以便快速查找
|
||||||
|
var stockMarketsDict = stockMarkets.ToDictionary(s => s.Id);
|
||||||
|
|
||||||
|
// 将StockModel转换为StockPriceRecordEntity
|
||||||
|
var priceRecords = new List<StockPriceRecordEntity>();
|
||||||
|
|
||||||
|
foreach (var stockModel in stockModels)
|
||||||
|
{
|
||||||
|
if (stockModel.Values == null || !stockModel.Values.Any())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从字典中查找股票信息,而不是每次查询数据库
|
||||||
|
if (!stockMarketsDict.TryGetValue(stockModel.Id, out var stockMarket))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个价格点创建一个记录
|
||||||
|
foreach (var priceValue in stockModel.Values)
|
||||||
|
{
|
||||||
|
var priceRecord = new StockPriceRecordEntity
|
||||||
|
{
|
||||||
|
StockId = stockMarket.Id,
|
||||||
|
CurrentPrice = priceValue,
|
||||||
|
Volume = 0, // 可以根据实际情况设置
|
||||||
|
Turnover = 0, // 可以根据实际情况设置
|
||||||
|
PeriodType = PeriodTypeEnum.Hour
|
||||||
|
};
|
||||||
|
|
||||||
|
priceRecords.Add(priceRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量保存价格记录
|
||||||
|
if (priceRecords.Any())
|
||||||
|
{
|
||||||
|
await BatchSaveStockPriceRecordsAsync(priceRecords);
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成最新股票记录
|
/// 生成最新股票记录
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task GenerateStocksAsync()
|
public async Task GenerateStocksAsync()
|
||||||
{
|
{
|
||||||
_skClient.RegisterPlugins<StockPlugins>("stock");
|
var question = """
|
||||||
await _skClient.ChatCompletionAsync("帮我生成多个股市内容");
|
根据给出的模拟新闻,模拟生成多家股票未来价格
|
||||||
|
|
||||||
|
以下是近期新闻:
|
||||||
|
ai势力逐步崛起
|
||||||
|
大量网红下岗
|
||||||
|
近期群星演唱会即将开幕
|
||||||
|
|
||||||
|
以下是多个股票:
|
||||||
|
伦敦股票:id:3a1886d5-1479-5402-3bcb-063e989898d1 最后一次价格:188.7
|
||||||
|
上海股票:id:3a1886d5-4393-606c-b040-52f1ef7d2158 最后一次价格:125.2
|
||||||
|
纽约股票:id:3a1886d5-6906-8f30-d955-198fbcfe4026 最后一次价格:185.5
|
||||||
|
|
||||||
|
根据上面信息,给每个股票返回未来24小时的股票价格,每个小时,整点为单位一条记录,一家共有24条,返回股票id 与 长度为24的价格列表
|
||||||
|
只用生成一次即可
|
||||||
|
""";
|
||||||
|
await _skClient.ChatCompletionAsync(question,("StockPlugins","save_stocks"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.SemanticKernel;
|
||||||
using Volo.Abp.Caching;
|
using Volo.Abp.Caching;
|
||||||
using Volo.Abp.Domain;
|
using Volo.Abp.Domain;
|
||||||
using Yi.Framework.Stock.Domain.Shared;
|
|
||||||
using Yi.Framework.Mapster;
|
using Yi.Framework.Mapster;
|
||||||
using Yi.Framework.Stock.Domain.Managers;
|
using Yi.Framework.Stock.Domain.Managers;
|
||||||
|
using Yi.Framework.Stock.Domain.Managers.SemanticKernel;
|
||||||
|
using Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins;
|
||||||
|
using Yi.Framework.Stock.Domain.Shared;
|
||||||
|
|
||||||
namespace Yi.Framework.Stock.Domain
|
namespace Yi.Framework.Stock.Domain
|
||||||
{
|
{
|
||||||
[DependsOn(
|
[DependsOn(
|
||||||
typeof(YiFrameworkStockDomainSharedModule),
|
typeof(YiFrameworkStockDomainSharedModule),
|
||||||
|
|
||||||
typeof(YiFrameworkMapsterModule),
|
typeof(YiFrameworkMapsterModule),
|
||||||
typeof(AbpDddDomainModule),
|
typeof(AbpDddDomainModule),
|
||||||
typeof(AbpCachingModule)
|
typeof(AbpCachingModule)
|
||||||
@@ -17,13 +21,30 @@ namespace Yi.Framework.Stock.Domain
|
|||||||
{
|
{
|
||||||
public override void ConfigureServices(ServiceConfigurationContext context)
|
public override void ConfigureServices(ServiceConfigurationContext context)
|
||||||
{
|
{
|
||||||
Configure<SemanticKernelOptions>((options)=>{
|
var configuration = context.Services.GetConfiguration();
|
||||||
options.Endpoint = "https://api.token-ai.cn/v1";
|
var services = context.Services;
|
||||||
options.ApiKey = "sk-V6OqmrloXDAiTM2FWoisGgaop72Ngr0fXAnXL8";
|
|
||||||
options.ModelId = "gpt-4o-mini";
|
// 配置绑定
|
||||||
options.MaxTokens = 1000;
|
var semanticKernelSection = configuration.GetSection("SemanticKernel");
|
||||||
options.PluginsDirectoryPath = "plugins";
|
services.Configure<SemanticKernelOptions>(configuration.GetSection("SemanticKernel"));
|
||||||
});
|
|
||||||
|
services.AddHttpClient();
|
||||||
|
#pragma warning disable SKEXP0010
|
||||||
|
// 从配置中获取值
|
||||||
|
var options = semanticKernelSection.Get<SemanticKernelOptions>();
|
||||||
|
services.AddKernel()
|
||||||
|
.AddOpenAIChatCompletion(
|
||||||
|
modelId: options.ModelId,
|
||||||
|
endpoint: new Uri(options.Endpoint),
|
||||||
|
apiKey: options.ApiKey);
|
||||||
|
#pragma warning restore SKEXP0010
|
||||||
|
|
||||||
|
// 添加插件
|
||||||
|
services.AddSingleton<KernelPlugin>(sp => KernelPluginFactory.CreateFromType<NewsPlugins>(serviceProvider: sp));
|
||||||
|
services.AddSingleton<KernelPlugin>(sp => KernelPluginFactory.CreateFromType<StockPlugins>(serviceProvider: sp));
|
||||||
|
|
||||||
|
// 注册NewsManager
|
||||||
|
services.AddTransient<NewsManager>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using OpenAI;
|
// using OpenAI;
|
||||||
using OpenAI.Managers;
|
// using OpenAI.Managers;
|
||||||
using OpenAI.ObjectModels;
|
// using OpenAI.ObjectModels;
|
||||||
using OpenAI.ObjectModels.RequestModels;
|
// using OpenAI.ObjectModels.RequestModels;
|
||||||
using OpenAI.ObjectModels.ResponseModels;
|
// using OpenAI.ObjectModels.ResponseModels;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
using Volo.Abp.Domain.Services;
|
using Volo.Abp.Domain.Services;
|
||||||
using Yi.Framework.ChatHub.Domain.Shared.Dtos;
|
using Yi.Framework.ChatHub.Domain.Shared.Dtos;
|
||||||
@@ -16,58 +16,59 @@ namespace Yi.Framework.ChatHub.Domain.Managers
|
|||||||
{
|
{
|
||||||
public AiManager(IOptions<AiOptions> options)
|
public AiManager(IOptions<AiOptions> options)
|
||||||
{
|
{
|
||||||
this.OpenAIService = new OpenAIService(new OpenAiOptions()
|
// this.OpenAIService = new OpenAIService(new OpenAiOptions()
|
||||||
{
|
// {
|
||||||
ApiKey = options.Value.ApiKey,
|
// ApiKey = options.Value.ApiKey,
|
||||||
BaseDomain = options.Value.BaseDomain
|
// BaseDomain = options.Value.BaseDomain
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
private OpenAIService OpenAIService { get; }
|
// private OpenAIService OpenAIService { get; }
|
||||||
|
|
||||||
public async IAsyncEnumerable<string> ChatAsStreamAsync(string model, List<AiChatContextDto> aiChatContextDtos)
|
public async IAsyncEnumerable<string> ChatAsStreamAsync(string model, List<AiChatContextDto> aiChatContextDtos)
|
||||||
{
|
{
|
||||||
if (aiChatContextDtos.Count == 0)
|
throw new NotImplementedException("准备sk重构");
|
||||||
{
|
yield break;
|
||||||
yield return null;
|
// if (aiChatContextDtos.Count == 0)
|
||||||
}
|
// {
|
||||||
|
// yield return null;
|
||||||
List<ChatMessage> messages = aiChatContextDtos.Select(x =>
|
// }
|
||||||
{
|
//
|
||||||
if (x.AnswererType == AnswererTypeEnum.Ai)
|
// List<ChatMessage> messages = aiChatContextDtos.Select(x =>
|
||||||
{
|
// {
|
||||||
return ChatMessage.FromSystem(x.Message);
|
// if (x.AnswererType == AnswererTypeEnum.Ai)
|
||||||
}
|
// {
|
||||||
else
|
// return ChatMessage.FromSystem(x.Message);
|
||||||
{
|
// }
|
||||||
return ChatMessage.FromUser(x.Message);
|
// else
|
||||||
}
|
// {
|
||||||
}).ToList();
|
// return ChatMessage.FromUser(x.Message);
|
||||||
var completionResult = OpenAIService.ChatCompletion.CreateCompletionAsStream(new ChatCompletionCreateRequest
|
// }
|
||||||
{
|
// }).ToList();
|
||||||
Messages = messages,
|
// var completionResult = OpenAIService.ChatCompletion.CreateCompletionAsStream(new ChatCompletionCreateRequest
|
||||||
Model =model
|
// {
|
||||||
});
|
// Messages = messages,
|
||||||
|
// Model =model
|
||||||
HttpStatusCode? error = null;
|
// });
|
||||||
await foreach (var result in completionResult)
|
//
|
||||||
{
|
// HttpStatusCode? error = null;
|
||||||
if (result.Successful)
|
// await foreach (var result in completionResult)
|
||||||
{
|
// {
|
||||||
yield return result.Choices.FirstOrDefault()?.Message.Content ?? null;
|
// if (result.Successful)
|
||||||
}
|
// {
|
||||||
else
|
// yield return result.Choices.FirstOrDefault()?.Message.Content ?? null;
|
||||||
{
|
// }
|
||||||
error = result.HttpStatusCode;
|
// else
|
||||||
break;
|
// {
|
||||||
}
|
// error = result.HttpStatusCode;
|
||||||
|
// break;
|
||||||
}
|
// }
|
||||||
if (error == HttpStatusCode.PaymentRequired)
|
//
|
||||||
{
|
// }
|
||||||
yield return "余额不足,请联系站长充值";
|
// if (error == HttpStatusCode.PaymentRequired)
|
||||||
|
// {
|
||||||
}
|
// yield return "余额不足,请联系站长充值";
|
||||||
|
//
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Betalgo.OpenAI" Version="8.6.1" />
|
|
||||||
<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)" />
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ namespace Yi.Abp.Application.Services
|
|||||||
public ISqlSugarRepository<BannerAggregateRoot> sqlSugarRepository { get; set; }
|
public ISqlSugarRepository<BannerAggregateRoot> sqlSugarRepository { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 动态Api
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name"></param>
|
/// <param name="name"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
|||||||
34
Yi.Abp.Net8/src/Yi.Abp.Web/Jobs/ai-stock/GenerateNewsJob.cs
Normal file
34
Yi.Abp.Net8/src/Yi.Abp.Web/Jobs/ai-stock/GenerateNewsJob.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Volo.Abp.BackgroundWorkers.Hangfire;
|
||||||
|
using Yi.Framework.Stock.Domain.Managers;
|
||||||
|
|
||||||
|
namespace Yi.Abp.Web.Jobs.ai_stock
|
||||||
|
{
|
||||||
|
public class GenerateNewsJob : HangfireBackgroundWorkerBase
|
||||||
|
{
|
||||||
|
private NewsManager _newsManager;
|
||||||
|
|
||||||
|
public GenerateNewsJob(NewsManager newsManager)
|
||||||
|
{
|
||||||
|
_newsManager = newsManager;
|
||||||
|
|
||||||
|
RecurringJobId = "AI股票新闻生成";
|
||||||
|
//每个小时整点执行一次
|
||||||
|
CronExpression = "0 0 * * * ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
|
||||||
|
{
|
||||||
|
// 每次触发只有2/24的概率执行生成新闻
|
||||||
|
var random = new Random();
|
||||||
|
var probability = random.Next(0, 24);
|
||||||
|
|
||||||
|
if (probability < 2)
|
||||||
|
{
|
||||||
|
await _newsManager.GenerateNewsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ using Volo.Abp.AspNetCore.VirtualFileSystem;
|
|||||||
using Volo.Abp.Auditing;
|
using Volo.Abp.Auditing;
|
||||||
using Volo.Abp.Autofac;
|
using Volo.Abp.Autofac;
|
||||||
using Volo.Abp.BackgroundJobs.Hangfire;
|
using Volo.Abp.BackgroundJobs.Hangfire;
|
||||||
|
using Volo.Abp.BackgroundWorkers;
|
||||||
using Volo.Abp.Caching;
|
using Volo.Abp.Caching;
|
||||||
using Volo.Abp.MultiTenancy;
|
using Volo.Abp.MultiTenancy;
|
||||||
using Volo.Abp.Swashbuckle;
|
using Volo.Abp.Swashbuckle;
|
||||||
@@ -106,6 +107,15 @@ namespace Yi.Abp.Web
|
|||||||
var host = context.Services.GetHostingEnvironment();
|
var host = context.Services.GetHostingEnvironment();
|
||||||
var service = context.Services;
|
var service = context.Services;
|
||||||
|
|
||||||
|
//本地开发环境,禁用作业执行
|
||||||
|
if (host.IsDevelopment())
|
||||||
|
{
|
||||||
|
Configure<AbpBackgroundWorkerOptions> (options =>
|
||||||
|
{
|
||||||
|
options.IsEnabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//请求日志
|
//请求日志
|
||||||
Configure<AbpAuditingOptions>(options =>
|
Configure<AbpAuditingOptions>(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -108,5 +108,13 @@
|
|||||||
"AiOptions": {
|
"AiOptions": {
|
||||||
"ApiKey": "",
|
"ApiKey": "",
|
||||||
"BaseDomain": ""
|
"BaseDomain": ""
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
//语义内核
|
||||||
|
"SemanticKernel": {
|
||||||
|
"ModelId": "gpt-4o",
|
||||||
|
"Endpoint": "https://xxx.com/v1",
|
||||||
|
"ApiKey": "sk-xxxxxx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
Yi.Bbs.Vue3/src/apis/stockApi.js
Normal file
49
Yi.Bbs.Vue3/src/apis/stockApi.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import request from "@/config/axios/service";
|
||||||
|
|
||||||
|
// 获取股票新闻列表
|
||||||
|
export function getStockNews(params) {
|
||||||
|
return request({
|
||||||
|
url: "/stock/news",
|
||||||
|
method: "get",
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户股票持仓
|
||||||
|
export function getUserHoldings() {
|
||||||
|
return request({
|
||||||
|
url: "/stock/user-holdings",
|
||||||
|
method: "get"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户交易记录
|
||||||
|
export function getUserTransactions(stockCode) {
|
||||||
|
return request({
|
||||||
|
url: "/stock/user-transactions",
|
||||||
|
method: "get",
|
||||||
|
params: { stockCode }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取股票价格记录
|
||||||
|
export function getStockPriceRecords(stockId, startTime, endTime, periodType = 'Hour') {
|
||||||
|
return request({
|
||||||
|
url: "/stock/price-records",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
StockId: stockId,
|
||||||
|
StartTime: startTime,
|
||||||
|
EndTime: endTime,
|
||||||
|
PeriodType: periodType
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取股市列表
|
||||||
|
export function getStockMarkets() {
|
||||||
|
return request({
|
||||||
|
url: "/stock/markets",
|
||||||
|
method: "get"
|
||||||
|
});
|
||||||
|
}
|
||||||
147
Yi.Bbs.Vue3/src/components/StockBoard/StockBoard.vue
Normal file
147
Yi.Bbs.Vue3/src/components/StockBoard/StockBoard.vue
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div class="stock-board">
|
||||||
|
<h2>股票看板</h2>
|
||||||
|
<div class="stock-list">
|
||||||
|
<div v-for="stock in stocks" :key="stock.code" class="stock-item">
|
||||||
|
<div class="stock-info">
|
||||||
|
<h3>{{ stock.name }} ({{ stock.code }})</h3>
|
||||||
|
<p :class="getPriceClass(stock)">¥{{ stock.currentPrice }}</p>
|
||||||
|
<p>涨跌幅: <span :class="getPriceClass(stock)">{{ stock.changePercent }}%</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="stock-actions">
|
||||||
|
<button @click="handleBuy(stock)">买入</button>
|
||||||
|
<button @click="handleSell(stock)">卖出</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 交易弹窗 -->
|
||||||
|
<div v-if="showTradeModal" class="trade-modal">
|
||||||
|
<h3>{{ tradeType === 'buy' ? '买入' : '卖出' }} {{ selectedStock.name }}</h3>
|
||||||
|
<p>当前价格: ¥{{ selectedStock.currentPrice }}</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>数量:</label>
|
||||||
|
<input v-model="tradeAmount" type="number" min="1" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>总金额: ¥{{ totalAmount }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button @click="confirmTrade">确认</button>
|
||||||
|
<button @click="cancelTrade">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useStocks } from '@/composables/useStocks'
|
||||||
|
import { useTrades } from '@/composables/useTrades'
|
||||||
|
|
||||||
|
const { stocks } = useStocks()
|
||||||
|
const { executeTrade } = useTrades()
|
||||||
|
|
||||||
|
const showTradeModal = ref(false)
|
||||||
|
const selectedStock = ref({})
|
||||||
|
const tradeType = ref('')
|
||||||
|
const tradeAmount = ref(1)
|
||||||
|
|
||||||
|
const totalAmount = computed(() => {
|
||||||
|
return (selectedStock.value.currentPrice * tradeAmount.value).toFixed(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
function getPriceClass(stock) {
|
||||||
|
return {
|
||||||
|
'price-up': stock.changePercent > 0,
|
||||||
|
'price-down': stock.changePercent < 0,
|
||||||
|
'price-unchanged': stock.changePercent === 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBuy(stock) {
|
||||||
|
selectedStock.value = stock
|
||||||
|
tradeType.value = 'buy'
|
||||||
|
showTradeModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSell(stock) {
|
||||||
|
selectedStock.value = stock
|
||||||
|
tradeType.value = 'sell'
|
||||||
|
showTradeModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmTrade() {
|
||||||
|
executeTrade({
|
||||||
|
stockCode: selectedStock.value.code,
|
||||||
|
stockName: selectedStock.value.name,
|
||||||
|
price: selectedStock.value.currentPrice,
|
||||||
|
amount: tradeAmount.value,
|
||||||
|
type: tradeType.value,
|
||||||
|
date: new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
cancelTrade()
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelTrade() {
|
||||||
|
showTradeModal.value = false
|
||||||
|
tradeAmount.value = 1
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stock-board {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-item {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-up {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-down {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-unchanged {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trade-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,18 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="stock-dashboard">
|
<div class="stock-dashboard">
|
||||||
<!-- 顶部选择器区域 -->
|
<!-- 顶部选择器区域 -->
|
||||||
<div class="stock-header">
|
<div class="stock-header">
|
||||||
<div class="title-area">
|
<div class="title-area">
|
||||||
<h2 class="title">意社区股市</h2>
|
<h2 class="title">意社区股市</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="selector-area">
|
<div class="selector-area">
|
||||||
<el-select v-model="currentStock" placeholder="选择股票" @change="changeStock" size="small">
|
<el-select
|
||||||
|
v-model="currentStock"
|
||||||
|
placeholder="选择股票"
|
||||||
|
@change="changeStock"
|
||||||
|
size="small"
|
||||||
|
:loading="isLoadingStockList"
|
||||||
|
:disabled="isLoadingStockList"
|
||||||
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in stockList"
|
v-for="item in stockList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.name"
|
:label="item.name"
|
||||||
:value="item.id">
|
:value="item.id">
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
<span class="stock-code">{{ item.code }}</span>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,10 +36,38 @@
|
|||||||
<h3>最新市场动态</h3>
|
<h3>最新市场动态</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="news-list">
|
<div class="news-list">
|
||||||
<div v-for="(news, index) in newsList" :key="index" class="news-item">
|
<div v-if="isLoadingNews && newsList.length === 0" class="loading-news">
|
||||||
<div class="news-date">{{ news.date }}</div>
|
<el-skeleton :rows="5" animated />
|
||||||
<div class="news-title">{{ news.title }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="news in newsList"
|
||||||
|
:key="news.id"
|
||||||
|
class="news-item"
|
||||||
|
>
|
||||||
|
<div class="news-date">
|
||||||
|
{{ dayjs(news.publishTime).format('YYYY-MM-DD') }}
|
||||||
|
<span class="news-source" v-if="news.source">· {{ news.source }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-title">{{ news.title }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="newsList.length === 0" class="empty-news">
|
||||||
|
暂无市场动态
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="newsTotal > newsList.length" class="news-more">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="loadMoreNews"
|
||||||
|
:loading="isLoadingNews"
|
||||||
|
>
|
||||||
|
加载更多
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,24 +77,30 @@
|
|||||||
<h3>{{ currentStockInfo.name }} ({{ currentStockInfo.code }})</h3>
|
<h3>{{ currentStockInfo.name }} ({{ currentStockInfo.code }})</h3>
|
||||||
<div class="price-info">
|
<div class="price-info">
|
||||||
<div class="current-price" :class="{'price-up': priceChange > 0, 'price-down': priceChange < 0}">
|
<div class="current-price" :class="{'price-up': priceChange > 0, 'price-down': priceChange < 0}">
|
||||||
{{ currentStockInfo.price }}
|
{{ currentStockInfo.price.toFixed(2) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="price-change" :class="{'price-up': priceChange > 0, 'price-down': priceChange < 0}">
|
<div class="price-change" :class="{'price-up': priceChange > 0, 'price-down': priceChange < 0}">
|
||||||
{{ priceChange > 0 ? '+' : '' }}{{ priceChange }} ({{ priceChangePercent }}%)
|
{{ priceChange > 0 ? '+' : '' }}{{ priceChange.toFixed(2) }} ({{ priceChangePercent }}%)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 股票图表 -->
|
<!-- 股票图表 -->
|
||||||
<div class="stock-chart">
|
<div class="stock-chart">
|
||||||
<StockChart :stockData="stockChartData" />
|
<div v-if="isLoadingChart" class="loading-chart">
|
||||||
|
<el-skeleton animated />
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<StockChart v-if="stockChartData.length > 0" :stockData="stockChartData" />
|
||||||
|
<el-empty v-else description="暂无股票数据" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 交易操作区 -->
|
<!-- 交易操作区 -->
|
||||||
<div class="trade-actions">
|
<div class="trade-actions">
|
||||||
<el-input-number v-model="tradeAmount" :min="1" :max="1000" label="数量"></el-input-number>
|
<el-button type="success" circle class="circle-button" @click="buyStock">买入</el-button>
|
||||||
<el-button type="success" @click="buyStock">买入</el-button>
|
<el-input-number v-model="tradeAmount" :min="1" :max="1000" label="数量" controls-position="right"></el-input-number>
|
||||||
<el-button type="danger" @click="sellStock">卖出</el-button>
|
<el-button type="danger" circle class="circle-button" @click="sellStock">卖出</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,14 +111,30 @@
|
|||||||
<h3>交易记录</h3>
|
<h3>交易记录</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-list">
|
<div class="history-list">
|
||||||
<div v-for="(record, index) in tradeHistory" :key="index" class="history-item">
|
<div v-if="isLoadingTradeHistory" class="loading-history">
|
||||||
<span class="time">{{ record.time }}</span>
|
<el-skeleton :rows="5" animated />
|
||||||
<span class="type" :class="record.type === 'buy' ? 'buy-type' : 'sell-type'">
|
|
||||||
{{ record.type === 'buy' ? '买入' : '卖出' }}
|
|
||||||
</span>
|
|
||||||
<span class="amount">{{ record.amount }}股</span>
|
|
||||||
<span class="price">¥{{ record.price }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="tradeHistory.length === 0" class="empty-history">
|
||||||
|
暂无交易记录
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(record, index) in tradeHistory" :key="index" class="history-item">
|
||||||
|
<div class="history-header">
|
||||||
|
<span class="stock-name">{{ record.stockName }}</span>
|
||||||
|
<span class="time">{{ record.time }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="history-details">
|
||||||
|
<span class="type" :class="record.type === 'buy' ? 'buy-type' : 'sell-type'">
|
||||||
|
{{ record.type === 'buy' ? '买入' : '卖出' }}
|
||||||
|
</span>
|
||||||
|
<span class="amount">{{ record.amount }}股</span>
|
||||||
|
<span class="price">¥{{ record.price }}</span>
|
||||||
|
<span class="total">总额: ¥{{ record.totalAmount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,48 +146,90 @@
|
|||||||
<h3>我的持仓</h3>
|
<h3>我的持仓</h3>
|
||||||
<span class="total-value">总资产: ¥{{ totalAssets }}</span>
|
<span class="total-value">总资产: ¥{{ totalAssets }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="portfolioList" stripe style="width: 100%">
|
|
||||||
<el-table-column prop="name" label="股票名称"></el-table-column>
|
<div v-if="isLoadingPortfolio" class="loading-portfolio">
|
||||||
<el-table-column prop="code" label="代码"></el-table-column>
|
<el-skeleton :rows="3" animated />
|
||||||
<el-table-column prop="amount" label="持有数量"></el-table-column>
|
</div>
|
||||||
<el-table-column prop="buyPrice" label="买入均价"></el-table-column>
|
|
||||||
<el-table-column prop="currentPrice" label="当前价格"></el-table-column>
|
<template v-else>
|
||||||
<el-table-column prop="profit" label="盈亏">
|
<el-empty v-if="portfolioList.length === 0" description="暂无持仓" />
|
||||||
<template #default="scope">
|
|
||||||
<span :class="scope.row.profit >= 0 ? 'price-up' : 'price-down'">
|
<el-table v-else :data="portfolioList" stripe style="width: 100%">
|
||||||
{{ scope.row.profit >= 0 ? '+' : '' }}{{ scope.row.profit }}
|
<el-table-column prop="name" label="股票名称"></el-table-column>
|
||||||
</span>
|
<el-table-column prop="code" label="代码"></el-table-column>
|
||||||
</template>
|
<el-table-column prop="amount" label="持有数量"></el-table-column>
|
||||||
</el-table-column>
|
<el-table-column prop="buyPrice" label="买入均价"></el-table-column>
|
||||||
</el-table>
|
<el-table-column prop="currentPrice" label="当前价格"></el-table-column>
|
||||||
|
<el-table-column prop="profit" label="盈亏">
|
||||||
|
<template #default="scope">
|
||||||
|
<span :class="parseFloat(scope.row.profit) >= 0 ? 'price-up' : 'price-down'">
|
||||||
|
{{ parseFloat(scope.row.profit) >= 0 ? '+' : '' }}{{ scope.row.profit }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted, reactive } from 'vue';
|
||||||
import StockChart from './components/StockChart.vue';
|
import StockChart from './components/StockChart.vue';
|
||||||
|
import { getStockNews, getUserHoldings, getUserTransactions, getStockPriceRecords, getStockMarkets } from '@/apis/stockApi.js';
|
||||||
|
import { dayjs } from 'element-plus';
|
||||||
|
|
||||||
// 股票列表数据
|
// 股票列表数据
|
||||||
const stockList = ref([
|
const stockList = ref([]);
|
||||||
{ id: 1, name: '阿里巴巴', code: 'BABA' },
|
const isLoadingStockList = ref(false);
|
||||||
{ id: 2, name: '腾讯控股', code: 'TCEHY' },
|
|
||||||
{ id: 3, name: '百度', code: 'BIDU' },
|
|
||||||
{ id: 4, name: '苹果', code: 'AAPL' },
|
|
||||||
{ id: 5, name: '微软', code: 'MSFT' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 当前选中的股票
|
// 当前选中的股票
|
||||||
const currentStock = ref(1);
|
const currentStock = ref('');
|
||||||
|
|
||||||
// 当前股票信息
|
// 当前股票信息
|
||||||
const currentStockInfo = ref({
|
const currentStockInfo = ref({
|
||||||
name: '阿里巴巴',
|
name: '',
|
||||||
code: 'BABA',
|
code: '',
|
||||||
price: 178.54,
|
price: 0,
|
||||||
prevClose: 175.23
|
prevClose: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取股市列表
|
||||||
|
const fetchStockMarkets = async () => {
|
||||||
|
try {
|
||||||
|
isLoadingStockList.value = true;
|
||||||
|
const { data } = await getStockMarkets();
|
||||||
|
|
||||||
|
// 将API返回的数据映射到组件所需的格式
|
||||||
|
stockList.value = (data.items || []).map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.marketName,
|
||||||
|
code: item.marketCode,
|
||||||
|
description: item.description,
|
||||||
|
state: item.state
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 如果有股票数据,默认选中第一个
|
||||||
|
if (stockList.value.length > 0) {
|
||||||
|
currentStock.value = stockList.value[0].id;
|
||||||
|
currentStockInfo.value = {
|
||||||
|
name: stockList.value[0].name,
|
||||||
|
code: stockList.value[0].code,
|
||||||
|
price: 0, // 将通过API更新
|
||||||
|
prevClose: 0 // 将通过API更新
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载默认选中股票的数据
|
||||||
|
await fetchStockPriceRecords();
|
||||||
|
await fetchTradeHistory();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取股市列表失败:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingStockList.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 价格变化
|
// 价格变化
|
||||||
const priceChange = computed(() => {
|
const priceChange = computed(() => {
|
||||||
return parseFloat((currentStockInfo.value.price - currentStockInfo.value.prevClose).toFixed(2));
|
return parseFloat((currentStockInfo.value.price - currentStockInfo.value.prevClose).toFixed(2));
|
||||||
@@ -143,67 +244,166 @@ const priceChangePercent = computed(() => {
|
|||||||
const tradeAmount = ref(1);
|
const tradeAmount = ref(1);
|
||||||
|
|
||||||
// 图表数据
|
// 图表数据
|
||||||
const stockChartData = ref([
|
const stockChartData = ref([]);
|
||||||
{ date: '2023-05-01', value: 170.23 },
|
const isLoadingChart = ref(false);
|
||||||
{ date: '2023-05-02', value: 172.45 },
|
|
||||||
{ date: '2023-05-03', value: 169.87 },
|
|
||||||
{ date: '2023-05-04', value: 173.25 },
|
|
||||||
{ date: '2023-05-05', value: 175.23 },
|
|
||||||
{ date: '2023-05-06', value: 178.54 }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 新闻列表
|
// 获取股票价格记录
|
||||||
const newsList = ref([
|
const fetchStockPriceRecords = async () => {
|
||||||
{ date: '2023-05-06', title: '央行宣布降低存款准备金率,股市普涨' },
|
try {
|
||||||
{ date: '2023-05-05', title: '科技巨头财报超预期,带动市场情绪高涨' },
|
isLoadingChart.value = true;
|
||||||
{ date: '2023-05-04', title: '美联储加息25个基点,符合市场预期' },
|
|
||||||
{ date: '2023-05-03', title: '新能源汽车销量持续增长,相关概念股走强' },
|
// 计算时间范围: 今天的近5个小时
|
||||||
{ date: '2023-05-02', title: '金融监管政策出台,银行板块承压' },
|
const now = dayjs();
|
||||||
{ date: '2023-05-01', title: '消费数据好于预期,零售业有望反弹' },
|
const endTime = now.format('YYYY-MM-DD HH:00');
|
||||||
{ date: '2023-04-30', title: '国内制造业PMI持续扩张,经济复苏势头强劲' },
|
const startTime = now.subtract(5, 'hour').format('YYYY-MM-DD HH:00');
|
||||||
{ date: '2023-04-29', title: '互联网巨头发布一季度财报,云业务成亮点' },
|
|
||||||
{ date: '2023-04-28', title: '全球芯片短缺问题缓解,半导体板块回调' },
|
// 获取当前选中股票的ID
|
||||||
{ date: '2023-04-27', title: '国际原油价格波动,能源股表现分化' }
|
// 注意:这里假设stockList中的id就是后端的stockId,如果不是需要调整
|
||||||
]);
|
const selectedStock = stockList.value.find(item => item.id === currentStock.value);
|
||||||
|
const stockId = selectedStock ? selectedStock.id : '';
|
||||||
|
|
||||||
|
if (!stockId) {
|
||||||
|
console.error('未找到有效的股票ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const { data } = await getStockPriceRecords(stockId, startTime, endTime);
|
||||||
|
const { data } = await getStockPriceRecords(stockId, '2025-03-08 21:00', '2025-03-09 21:00');
|
||||||
|
// 将API返回的数据映射到图表所需的格式
|
||||||
|
stockChartData.value = (data.items || []).map(item => ({
|
||||||
|
date: dayjs(item.recordTime).format('MM-DD HH'),
|
||||||
|
value: item.currentPrice
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 如果有数据,更新当前股票信息
|
||||||
|
if (stockChartData.value.length > 0) {
|
||||||
|
const latestRecord = stockChartData.value[stockChartData.value.length - 1];
|
||||||
|
currentStockInfo.value.price = latestRecord.value;
|
||||||
|
// 如果只有一条记录,前收盘价设为当前价格,否则设为前一条记录的价格
|
||||||
|
currentStockInfo.value.prevClose = stockChartData.value.length > 1
|
||||||
|
? stockChartData.value[stockChartData.value.length - 2].value
|
||||||
|
: latestRecord.value;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取股票价格记录失败:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingChart.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新闻列表查询参数
|
||||||
|
const newsQuery = reactive({
|
||||||
|
skipCount: 1,
|
||||||
|
maxResultCount: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
// 新闻列表数据
|
||||||
|
const newsList = ref([]);
|
||||||
|
const newsTotal = ref(0);
|
||||||
|
const isLoadingNews = ref(false);
|
||||||
|
|
||||||
|
// 加载更多新闻
|
||||||
|
const loadMoreNews = async () => {
|
||||||
|
newsQuery.skipCount += 1;
|
||||||
|
await fetchNewsList(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取新闻列表
|
||||||
|
const fetchNewsList = async (isReset = true) => {
|
||||||
|
try {
|
||||||
|
isLoadingNews.value = true;
|
||||||
|
const { data } = await getStockNews(newsQuery);
|
||||||
|
|
||||||
|
if (isReset) {
|
||||||
|
newsList.value = data.items || [];
|
||||||
|
} else {
|
||||||
|
newsList.value = [...newsList.value, ...(data.items || [])];
|
||||||
|
}
|
||||||
|
|
||||||
|
newsTotal.value = data.totalCount || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取新闻列表失败:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingNews.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 交易记录
|
// 交易记录
|
||||||
const tradeHistory = ref([
|
const tradeHistory = ref([]);
|
||||||
{ time: '2023-05-06 14:30:25', type: 'buy', amount: 100, price: 178.54 },
|
const isLoadingTradeHistory = ref(false);
|
||||||
{ time: '2023-05-06 11:25:18', type: 'sell', amount: 50, price: 177.89 },
|
|
||||||
{ time: '2023-05-05 15:42:36', type: 'buy', amount: 200, price: 175.23 },
|
// 获取交易记录
|
||||||
{ time: '2023-05-05 10:15:42', type: 'buy', amount: 150, price: 174.56 },
|
const fetchTradeHistory = async () => {
|
||||||
{ time: '2023-05-04 13:28:57', type: 'sell', amount: 100, price: 173.25 }
|
try {
|
||||||
]);
|
isLoadingTradeHistory.value = true;
|
||||||
|
const { data } = await getUserTransactions();
|
||||||
|
|
||||||
|
// 将API返回的数据映射到组件所需的格式
|
||||||
|
tradeHistory.value = (data.items || []).map(item => ({
|
||||||
|
time: dayjs(item.creationTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
type: item.transactionType.toLowerCase(), // 转为小写以匹配现有的CSS类
|
||||||
|
amount: item.quantity,
|
||||||
|
price: item.price.toFixed(2),
|
||||||
|
stockName: item.stockName,
|
||||||
|
totalAmount: item.totalAmount.toFixed(2)
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取交易记录失败:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingTradeHistory.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 持仓列表
|
// 持仓列表
|
||||||
const portfolioList = ref([
|
const portfolioList = ref([]);
|
||||||
{ name: '阿里巴巴', code: 'BABA', amount: 200, buyPrice: 172.45, currentPrice: 178.54, profit: 1218 },
|
const isLoadingPortfolio = ref(false);
|
||||||
{ name: '腾讯控股', code: 'TCEHY', amount: 300, buyPrice: 68.25, currentPrice: 71.35, profit: 930 },
|
|
||||||
{ name: '百度', code: 'BIDU', amount: 150, buyPrice: 132.78, currentPrice: 128.65, profit: -619.5 }
|
// 获取持仓列表
|
||||||
]);
|
const fetchPortfolioList = async () => {
|
||||||
|
try {
|
||||||
|
isLoadingPortfolio.value = true;
|
||||||
|
const { data } = await getUserHoldings();
|
||||||
|
|
||||||
|
// 将API返回的数据映射到组件所需的格式
|
||||||
|
portfolioList.value = (data.items || []).map(item => ({
|
||||||
|
name: item.stockName,
|
||||||
|
code: item.stockCode,
|
||||||
|
amount: item.quantity,
|
||||||
|
buyPrice: item.costPrice.toFixed(2),
|
||||||
|
currentPrice: item.currentPrice.toFixed(2),
|
||||||
|
profit: item.profitLoss.toFixed(2)
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取持仓列表失败:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingPortfolio.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 总资产
|
// 总资产
|
||||||
const totalAssets = computed(() => {
|
const totalAssets = computed(() => {
|
||||||
const stockValue = portfolioList.value.reduce((sum, stock) => {
|
const stockValue = portfolioList.value.reduce((sum, stock) => {
|
||||||
return sum + stock.currentPrice * stock.amount;
|
return sum + parseFloat(stock.currentPrice) * stock.amount;
|
||||||
}, 0);
|
}, 0);
|
||||||
return (stockValue + 50000).toFixed(2); // 假设有50000现金
|
return (stockValue + 50000).toFixed(2); // 假设有50000现金
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换股票
|
// 切换股票
|
||||||
const changeStock = (stockId) => {
|
const changeStock = async (stockId) => {
|
||||||
// 模拟获取新的股票数据
|
|
||||||
const stock = stockList.value.find(item => item.id === stockId);
|
const stock = stockList.value.find(item => item.id === stockId);
|
||||||
if (stock) {
|
if (stock) {
|
||||||
currentStockInfo.value = {
|
currentStockInfo.value = {
|
||||||
name: stock.name,
|
name: stock.name,
|
||||||
code: stock.code,
|
code: stock.code,
|
||||||
price: parseFloat((150 + Math.random() * 50).toFixed(2)),
|
price: 0, // 临时值,将通过API更新
|
||||||
prevClose: parseFloat((150 + Math.random() * 50).toFixed(2))
|
prevClose: 0 // 临时值,将通过API更新
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模拟更新图表数据
|
// 更新图表数据
|
||||||
// 实际项目中应该通过API获取
|
await fetchStockPriceRecords();
|
||||||
|
|
||||||
|
// 更新交易记录
|
||||||
|
await fetchTradeHistory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,8 +420,17 @@ const sellStock = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 生命周期钩子
|
// 生命周期钩子
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
// 初始化数据,可以从API获取
|
|
||||||
|
// 先获取股市列表
|
||||||
|
await fetchStockMarkets();
|
||||||
|
|
||||||
|
// 初始化数据,从API获取
|
||||||
|
await Promise.all([
|
||||||
|
fetchNewsList(),
|
||||||
|
fetchPortfolioList(),
|
||||||
|
fetchTradeHistory()
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -275,8 +484,8 @@ onMounted(() => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 2fr 1fr;
|
grid-template-columns: 1fr 2fr 1fr;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
height: 52vh;
|
height: 60vh;
|
||||||
min-height: 320px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-section, .stock-panel, .trade-history {
|
.news-section, .stock-panel, .trade-history {
|
||||||
@@ -314,13 +523,15 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.news-item {
|
.news-item {
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
border-bottom: 1px solid #30363d;
|
border-bottom: 1px solid #30363d;
|
||||||
transition: background-color 0.3s;
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-item:hover {
|
.news-item:hover {
|
||||||
background-color: #1c2128;
|
background-color: #1c2128;
|
||||||
|
transform: translateX(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-date {
|
.news-date {
|
||||||
@@ -331,21 +542,31 @@ onMounted(() => {
|
|||||||
|
|
||||||
.news-title {
|
.news-title {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stock-info {
|
.stock-info {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-info h3 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-info {
|
.price-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-price {
|
.current-price {
|
||||||
font-size: 2em;
|
font-size: 1.6em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,14 +580,25 @@ onMounted(() => {
|
|||||||
|
|
||||||
.stock-chart {
|
.stock-chart {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 15px 0;
|
margin: 10px 0;
|
||||||
|
height: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trade-actions {
|
.trade-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
align-items: center;
|
||||||
margin-top: 15px;
|
gap: 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-button {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-list {
|
.history-list {
|
||||||
@@ -375,18 +607,47 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.history-item {
|
.history-item {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 10px;
|
margin-bottom: 8px;
|
||||||
border-bottom: 1px solid #30363d;
|
}
|
||||||
|
|
||||||
|
.stock-name {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buy-type {
|
.buy-type {
|
||||||
color: #3fb950;
|
color: #3fb950;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sell-type {
|
.sell-type {
|
||||||
color: #f85149;
|
color: #f85149;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
margin-left: auto;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portfolio {
|
.portfolio {
|
||||||
@@ -438,9 +699,108 @@ onMounted(() => {
|
|||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-input-number) {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 调整表格最大高度 */
|
/* 调整表格最大高度 */
|
||||||
:deep(.el-table__body-wrapper) {
|
:deep(.el-table__body-wrapper) {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: calc(35vh - 80px);
|
max-height: calc(35vh - 80px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.news-source {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: #58a6ff;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-news, .empty-news {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-portfolio {
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-empty {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-history, .empty-history {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-chart {
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-code {
|
||||||
|
float: right;
|
||||||
|
color: #8b949e;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-area :deep(.el-select) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改骨架屏颜色,使其适合深色主题 */
|
||||||
|
.stock-dashboard :deep(.el-skeleton__item) {
|
||||||
|
background-color: #30363d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-dashboard :deep(.el-skeleton__paragraph) {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-dashboard :deep(.el-skeleton__p) {
|
||||||
|
background-color: #30363d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-dashboard :deep(.el-skeleton__first-line),
|
||||||
|
.stock-dashboard :deep(.el-skeleton__paragraph > li) {
|
||||||
|
background-color: #30363d !important;
|
||||||
|
height: 16px !important;
|
||||||
|
margin-top: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 骨架屏动画效果调整 */
|
||||||
|
.stock-dashboard :deep(.is-animated .el-skeleton__item) {
|
||||||
|
background: linear-gradient(90deg, #30363d 25%, #383f47 37%, #30363d 63%) !important;
|
||||||
|
background-size: 400% 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-chart .el-empty{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-dashboard :deep(.el-empty) {
|
||||||
|
height: auto;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-dashboard :deep(.el-empty__image) {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-dashboard :deep(.el-empty__description) {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -149,5 +149,21 @@ onMounted(() => {
|
|||||||
.stock-chart-container {
|
.stock-chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-empty) {
|
||||||
|
padding: 20px 0;
|
||||||
|
height: auto;
|
||||||
|
max-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-empty__image) {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-empty__description) {
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user