Compare commits

...

28 Commits

Author SHA1 Message Date
spark
4d541e81a2 feat(frontend):添加鸣谢部分
- 增加了捐赠者、开发者和开源项目的鸣谢列表
- 优化了关于页面的布局,使鸣谢内容更加突出
- 添加了外部链接,方便用户访问相关开源项目
2025-02-09 16:04:45 +08:00
spark
6dfe3fd135 feat(frontend):添加鸣谢部分
- 增加了捐赠者、开发者和开源项目的鸣谢列表
- 优化了关于页面的布局,使鸣谢内容更加突出
- 添加了外部链接,方便用户访问相关开源项目
2025-02-09 16:01:47 +08:00
spark
915e12eab3 feat(frontend):丰富关于页面内容并优化布局
- 在 GitHub 链接旁边添加 Issues 和 Releases 链接
- 在邮箱下方添加 QQ 和微信联系方式- 使用 n-divider 组件进行垂直分割,提高可读性
2025-02-09 15:40:04 +08:00
spark
bcfcbfeef0 fix(stock):优化股票关注功能
- 增加股票代码有效性验证
- 改进关注失败时的错误处理和用户提示
- 修复可能的 nil pointer dereference 问题
2025-02-09 15:29:04 +08:00
spark
1dc731de1e refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:50:13 +08:00
spark
a580f9254a refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:44:39 +08:00
spark
9b080bbb45 refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:26:10 +08:00
spark
86183f4585 build:更新Wails构建动作版本 2025-02-08 16:48:12 +08:00
spark
97ab29259a ci:精简 commit message 输出
- 修改了获取 commit message 的 git 命令,仅保留 commit 主题行
-移除了不必要的信息,如作者和日期
- 优化了输出格式,提高了可读性
2025-02-08 16:30:54 +08:00
spark
91f3e66239 ci: 更新获取 commit message 的命令
- 修复了获取 commit message 时的语法错误
- 使用 PowerShell 兼容的命令格式
2025-02-08 16:06:59 +08:00
Lovegood
713b25d2db Update main.yml 2025-02-08 15:46:58 +08:00
spark
d0b65e7063 ci: 更新获取 commit message 的命令
- 修复了获取 commit message 时的语法错误
- 使用 PowerShell 兼容的命令格式
2025-02-08 15:34:40 +08:00
spark
f062306158 build: 更新 Wails 构建动作版本
- 将 ArvinLovegood/wails-build-action 版本从 v2.5 升级到 v2.6
- 保持其他配置不变,仅更新动作版本
2025-02-08 15:25:35 +08:00
spark
ae7b617e83 feat(frontend): 添加关于软件页面并实现版本信息动态获取
- 新增 about.vue 组件,包含软件介绍、更新说明和作者信息
- 添加 GetVersionInfo 函数,用于获取版本信息
- 在 App.vue 中添加关于软件的菜单项
- 在 router.js 中添加关于软件的路由
- 优化页面布局和样式
2025-02-08 15:05:52 +08:00
spark
1035f2a800 feat(frontend): 添加关于软件页面
- 在 App.vue 中添加关于软件的菜单项
- 在 router.js 中添加关于软件的路由- 新增 about.vue 组件,包含软件介绍和作者信息
2025-02-08 12:20:40 +08:00
spark
cb28b18541 docs(README): 更新 AI 分析股票功能截图
- 将 AI 分析股票功能的截图从 img_10.png 修改为 img.png
- 更新 README.md 中的相关图片引用
2025-02-08 11:39:39 +08:00
spark
9d42eb2729 docs(README): 更新项目介绍和赞助信息 2025-02-08 11:24:56 +08:00
spark
7b93d4d8ca feat(data): 添加 AIResponseResult模型并实现相关功能
感谢 @gnim2600 的建议!

- 新增 AIResponseResult 模型用于保存 AI 分析结果
- 实现 SaveAIResponseResult 和 GetAIResponseResult 函数
- 在前端添加 AI 分析功能,包括保存和获取分析结果
-优化 AI 分析界面,增加分析时间显示和再次分析按钮
2025-02-08 11:13:17 +08:00
spark
3e13ef007b feat(openai): 添加 OpenAI API 超时设置并调整相关功能
感谢@gnim2600 @XXXiaohuayanGGG 两位提供的帮助和建议
- 在前端和后端添加 OpenAI API 超时设置选项
- 更新 AI 诊断股票功能,支持自定义超时时间
- 优化设置界面布局,提高用户体验
- 为 AI 分析结果添加居中显示样式
2025-02-08 09:14:09 +08:00
spark
6ff1b68f1b fix:修复 GitHub 时间转换错误
- 移除了 getTimezoneOffset() * 60 * 1000 的计算
-现在直接使用 utcDate.getTime() 获取时间戳
2025-02-07 11:21:05 +08:00
spark
a6547db195 docs(README): 添加版本信息提示截图
- 在 README.md 中新增了版本信息提示部分
- 添加了对应的截图 img_11.png
2025-02-07 11:03:01 +08:00
spark
567414a136 feat(update): 增加新版本详细信息和发布时间
- 获取并显示新版本的 Tag 和 Commit 信息
- 将 UTC 时间转换为本地时间并显示
- 在通知中添加新版本详细信息和发布时间
- 优化股票卡片样式,增加鼠标悬停效果
2025-02-07 10:49:55 +08:00
Lovegood
34dc38a95f Merge pull request #3 from 2lovecode/feature-support-macos
feat(macos):support macos
2025-02-06 20:12:38 +08:00
2lovecode
6d2ab3ef41 feat(macos):support macos 2025-02-06 18:03:06 +08:00
spark
e55506705e feat(update): 添加软件更新检查功能
- 在应用启动时检查 GitHub 上的最新版本
- 如果发现新版本,通过通知提示用户更新
- 新增 GitHubReleaseVersion模型用于解析版本信息
- 在前端添加更新通知的展示逻辑
2025-02-06 16:19:11 +08:00
spark
322e87efbd ci:为 GitHub Actions 添加 build-tags 参数
在 GitHub Actions 的构建配置中添加了 build-tags 参数,使其等于当前的引用名称(github.ref_name)。这允许我们在构建过程中使用特定的标记。

- 修改了 .github/workflows/main.yml 文件- 在 build-platform 部分添加了 build-tags 参数
- 参数值设置为当前引用名称,增加了构建的灵活性和可追溯性
2025-02-06 15:01:36 +08:00
spark
1628381295 feat(app): 添加版本信息,为更新推送做准备
- 在应用启动时打印版本号
2025-02-06 14:53:07 +08:00
spark
8afc26badb test:移除雪球和硅流 API 调用相关代码 2025-02-05 16:44:00 +08:00
24 changed files with 1213 additions and 212 deletions

View File

@@ -31,11 +31,19 @@ jobs:
with:
submodules: recursive
- name: Get commit message
id: get_commit_message
run: |
$commit_message = & git log -1 --pretty=format:"%s"
echo "::set-output name=commit_message::$commit_message"
- name: Build wails
uses: ArvinLovegood/wails-build-action@v2.3
uses: ArvinLovegood/wails-build-action@v2.8
id: build
with:
build-name: ${{ matrix.build.name }}
build-platform: ${{ matrix.build.platform }}
package: true
go-version: '1.23'
build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}

View File

@@ -1,9 +1,11 @@
![Wails and NaiveUI](./build/appicon.png)
## 自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具
经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
- 欢迎大家提出宝贵的建议欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧),谢谢。
## BIG NEWS !!! 重大更新!!!
- 2025.01.17 新增AI大模型分析股票功能
![img_5.png](build/screenshot/img_10.png)
![img_5.png](build/screenshot/img.png)
## 简介
- 本项目基于Wails和NaiveUI纯属无聊仅供娱乐不喜勿喷。
@@ -23,8 +25,9 @@
### 钉钉报警通知
![img_4.png](build/screenshot/img_5.png)
### AI分析股票
![img_5.png](build/screenshot/img_10.png)
![img_5.png](build/screenshot/img.png)
### 版本信息提示
![img_11.png](build/screenshot/img_11.png)
## About
### A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).

61
app.go
View File

@@ -4,6 +4,7 @@ package main
import (
"context"
"encoding/base64"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
@@ -16,6 +17,7 @@ import (
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"time"
)
@@ -37,6 +39,7 @@ func NewApp() *App {
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here
a.ctx = ctx
@@ -49,6 +52,36 @@ func (a *App) startup(ctx context.Context) {
}
func checkUpdate(a *App) {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
if err != nil {
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
tag := &models.Tag{}
_, err = resty.New().R().
SetResult(tag).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/git/ref/tags/" + releaseVersion.TagName)
if err == nil {
releaseVersion.Tag = *tag
}
commit := &models.Commit{}
_, err = resty.New().R().
SetResult(commit).
Get(tag.Object.Url)
if err == nil {
releaseVersion.Commit = *commit
}
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
}
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
@@ -81,6 +114,11 @@ func (a *App) domReady(ctx context.Context) {
}()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
go MonitorStockPrices(a)
//检查新版本
go func() {
checkUpdate(a)
}()
}
func refreshTelegraphList() *[]string {
@@ -375,10 +413,6 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChat(stock string) string {
return data.NewDeepSeekOpenAi().NewChat(stock)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
for msg := range msgs {
@@ -387,6 +421,25 @@ func (a *App) NewChatStream(stock, stockCode string) {
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func (a *App) SaveAIResponseResult(stockCode, stockName, result string) {
data.NewDeepSeekOpenAi().SaveAIResponseResult(stockCode, stockName, result)
}
func (a *App) GetAIResponseResult(stock string) *models.AIResponseResult {
return data.NewDeepSeekOpenAi().GetAIResponseResult(stock)
}
func (a *App) GetVersionInfo() *models.VersionInfo {
return &models.VersionInfo{
Version: Version,
Icon: GetImageBase(icon),
Content: VersionCommit,
}
}
func GetImageBase(bytes []byte) string {
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(bytes)
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {

459
app_darwin.go Normal file
View File

@@ -0,0 +1,459 @@
//go:build darwin
// +build darwin
package main
import (
"context"
"fmt"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice"
"github.com/go-resty/resty/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
return &App{
cache: cache,
}
}
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here
a.ctx = ctx
// TODO 创建系统托盘
}
func checkUpdate(a *App) {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
if err != nil {
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
}
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
//定时更新数据
go func() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
interval := config.RefreshInterval
if interval <= 0 {
interval = 1
}
ticker := time.NewTicker(time.Second * time.Duration(interval))
defer ticker.Stop()
for range ticker.C {
if isTradingTime(time.Now()) {
MonitorStockPrices(a)
}
}
}()
go func() {
ticker := time.NewTicker(time.Second * time.Duration(60))
defer ticker.Stop()
for range ticker.C {
telegraph := refreshTelegraphList()
if telegraph != nil {
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
}
}
}()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
go MonitorStockPrices(a)
//检查新版本
go func() {
checkUpdate(a)
}()
}
func refreshTelegraphList() *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, selection.Text())
})
return &telegraph
}
// isTradingDay 判断是否是交易日
func isTradingDay(date time.Time) bool {
weekday := date.Weekday()
// 判断是否是周末
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
// 这里可以添加具体的节假日判断逻辑
// 例如:判断是否是春节、国庆节等
return true
}
// isTradingTime 判断是否是交易时间
func isTradingTime(date time.Time) bool {
if !isTradingDay(date) {
return false
}
hour, minute, _ := date.Clock()
// 判断是否在9:15到11:30之间
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
return true
}
// 判断是否在13:00到15:00之间
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
return true
}
return false
}
func MonitorStockPrices(a *App) {
dest := &[]data.FollowedStock{}
db.Dao.Model(&data.FollowedStock{}).Find(dest)
total := float64(0)
//for _, follow := range *dest {
// stockData := getStockInfo(follow)
// total += stockData.ProfitAmountToday
// price, _ := convertor.ToFloat(stockData.Price)
// if stockData.PrePrice != price {
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
// }
//}
stockInfos := GetStockInfos(*dest...)
for _, stockInfo := range *stockInfos {
total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price {
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
}
}
if total != 0 {
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
// systray.SetTooltip(title)
}
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
//runtime.WindowSetTitle(a.ctx, title)
}
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockCodes := make([]string, 0)
for _, follow := range follows {
stockCodes = append(stockCodes, follow.StockCode)
}
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
if err != nil {
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
return nil
}
stockInfos := make([]data.StockInfo, 0)
for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
return follow.StockCode == info.Code
})
if ok {
addStockFollowData(v, &info)
stockInfos = append(stockInfos, info)
}
}
return &stockInfos
}
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
stockCode := follow.StockCode
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
if err != nil || len(*stockDatas) == 0 {
return &data.StockInfo{}
}
stockData := (*stockDatas)[0]
addStockFollowData(follow, &stockData)
return &stockData
}
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.PrePrice = follow.Price //上次当前价格
stockData.Sort = follow.Sort
stockData.CostPrice = follow.CostPrice //成本价
stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice
//当前价格
price, _ := convertor.ToFloat(stockData.Price)
//当前价格为0 时 使用卖一价格作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.A1P)
}
//当前价格依然为0 时 使用买一报价作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.B1P)
}
//昨日收盘价
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
//当前价格依然为0 时 使用昨日收盘价为当前价格
if price == 0 {
price = preClosePrice
}
//今日最高价
highPrice, _ := convertor.ToFloat(stockData.High)
if highPrice == 0 {
highPrice, _ = convertor.ToFloat(stockData.Open)
}
//今日最低价
lowPrice, _ := convertor.ToFloat(stockData.Low)
if lowPrice == 0 {
lowPrice, _ = convertor.ToFloat(stockData.Open)
}
//开盘价
//openPrice, _ := convertor.ToFloat(stockData.Open)
if price > 0 {
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
}
if highPrice > 0 {
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
}
if lowPrice > 0 {
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
}
if follow.CostPrice > 0 && follow.Volume > 0 {
if price > 0 {
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
} else {
//未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
}
}
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
if follow.Price != price && price > 0 {
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
"price": price,
})
}
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
if err != nil {
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
}
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
// systray.Quit()
}
// Greet returns a greeting for the given name
func (a *App) Greet(stockCode string) *data.StockInfo {
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
follow := &data.FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
stockInfo := getStockInfo(*follow)
return stockInfo
}
func (a *App) Follow(stockCode string) string {
return data.NewStockDataApi().Follow(stockCode)
}
func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode)
}
func (a *App) GetFollowList() []data.FollowedStock {
return data.NewStockDataApi().GetFollowList()
}
func (a *App) GetStockList(key string) []data.StockBasic {
return data.NewStockDataApi().GetStockList(key)
}
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
stockInfo := &data.StockInfo{}
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChat(stock string) string {
return data.NewDeepSeekOpenAi().NewChat(stock)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func (a *App) UpdateConfig(settings *data.Settings) string {
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

View File

@@ -0,0 +1,52 @@
//go:build darwin
// +build darwin
package data
import (
"fmt"
"go-stock/backend/logger"
"os/exec"
)
// AlertWindowsApi @Author 2lovecode
// @Date 2025/02/06 17:50
// @Desc
// -----------------------------------------------------------------------------------
type AlertWindowsApi struct {
AppID string
// 窗口标题
Title string
// 窗口内容
Content string
// 窗口图标
Icon string
}
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
return &AlertWindowsApi{
AppID: AppID,
Title: Title,
Content: Content,
Icon: Icon,
}
}
func (a AlertWindowsApi) SendNotification() bool {
if getConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}
script := fmt.Sprintf(`display notification "%s" with title "%s"`, a.Content, a.Title)
cmd := exec.Command("osascript", "-e", script)
err := cmd.Run()
if err != nil {
logger.SugaredLogger.Error(err)
return false
}
return true
}

View File

@@ -0,0 +1,32 @@
//go:build darwin
// +build darwin
package data
import (
"go-stock/backend/logger"
"testing"
"github.com/go-toast/toast"
)
// @Author 2lovecode
// @Date 2025/02/06 17:50
// @Desc
// -----------------------------------------------------------------------------------
func TestAlert(t *testing.T) {
notification := toast.Notification{
AppID: "go-stock",
Title: "Hello, World!",
Message: "This is a toast notification.",
Icon: "../../build/appicon.png",
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return
}
}

View File

@@ -9,7 +9,9 @@ import (
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"sync"
"time"
@@ -26,6 +28,7 @@ type OpenAi struct {
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature"`
Prompt string `json:"prompt"`
TimeOut int `json:"time_out"`
}
func NewDeepSeekOpenAi() *OpenAi {
@@ -37,6 +40,7 @@ func NewDeepSeekOpenAi() *OpenAi {
MaxTokens: config.OpenAiMaxTokens,
Temperature: config.OpenAiTemperature,
Prompt: config.Prompt,
TimeOut: config.OpenAiApiTimeOut,
}
}
@@ -70,60 +74,6 @@ type AiResponse struct {
SystemFingerprint string `json:"system_fingerprint"`
}
func (o OpenAi) NewChat(stock string) string {
client := resty.New()
client.SetBaseURL(o.BaseUrl)
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
res := &AiResponse{}
_, err := client.R().
SetResult(res).
SetBody(map[string]interface{}{
"model": o.Model,
"max_tokens": o.MaxTokens,
"temperature": o.Temperature,
"messages": []map[string]interface{}{
{
"role": "system",
"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:" +
"1. 市场背景:\n" +
"- 当前A股市场整体走势(如:牛市、熊市、震荡市)\n " +
"- 近期影响市场的主要宏观经济因素\n " +
"- 市场情绪指标(如:融资融券余额、成交量变化) " +
"2. 技术指标分析: " +
"- 当前股价水平" +
"- 所在boll区间" +
"- 上证指数的MA(移动平均线)、MACD、KDJ指标分析\n " +
"- 行业板块轮动情况\n " +
"- 近期市场热点和龙头股票的技术形态 " +
"3. 风险评估:\n " +
"- 当前市场主要风险因素\n " +
"- 如何设置止损和止盈位\n " +
"- 资金管理建议(如:仓位控制) " +
"4. 投资策略:\n " +
"- 短期(1-2周)、中期(1-3月)和长期(3-6月)的市场预期\n " +
"- 不同风险偏好投资者的策略建议\n " +
"- 值得关注的行业板块和个股推荐(请给出2-3个具体例子,包括股票代码和名称) " +
"5. 技术面和基本面结合:\n " +
"- 如何将技术分析与公司基本面分析相结合\n " +
"- 识别高质量股票的关键指标 " +
"请提供详细的分析和具体的操作建议,包括入场时机、持仓周期和退出策略。同时,请强调风险控制的重要性,并提醒投资者需要根据自身情况做出决策。 " +
"你的分析和建议应当客观、全面,并基于当前可获得的市场数据。如果某些信息无法确定,请明确指出并解释原因。",
},
{
"role": "user",
"content": "点评一下" + stock,
},
},
}).
Post("/chat/completions")
if err != nil {
return ""
}
//logger.SugaredLogger.Infof("%v", res.Choices[0].Message.Content)
return res.Choices[0].Message.Content
}
func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
ch := make(chan string, 512)
go func() {
@@ -136,10 +86,10 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
"content": o.Prompt,
},
}
logger.SugaredLogger.Infof("Prompt%s", o.Prompt)
wg := &sync.WaitGroup{}
wg.Add(5)
go func() {
defer wg.Done()
messages := SearchStockPriceInfo(stockCode)
@@ -206,7 +156,10 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3)
client.SetTimeout(1 * time.Minute)
if o.TimeOut <= 0 {
o.TimeOut = 300
}
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
resp, err := client.R().
SetDoNotParseResponse(true).
SetBody(map[string]interface{}{
@@ -444,3 +397,17 @@ func GetTelegraphList() *[]string {
})
return &telegraph
}
func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result string) {
db.Dao.Create(&models.AIResponseResult{
StockCode: stockCode,
StockName: stockName,
Content: result,
})
}
func (o OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
var result models.AIResponseResult
db.Dao.Where("stock_code = ?", stock).Order("id desc").First(&result)
return &result
}

View File

@@ -21,7 +21,9 @@ type Settings struct {
OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"`
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
}
func (receiver Settings) TableName() string {
@@ -56,6 +58,8 @@ func (s SettingsApi) UpdateConfig() string {
"open_ai_temperature": s.Config.OpenAiTemperature,
"tushare_token": s.Config.TushareToken,
"prompt": s.Config.Prompt,
"check_update": s.Config.CheckUpdate,
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
})
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
@@ -73,6 +77,8 @@ func (s SettingsApi) UpdateConfig() string {
OpenAiTemperature: s.Config.OpenAiTemperature,
TushareToken: s.Config.TushareToken,
Prompt: s.Config.Prompt,
CheckUpdate: s.Config.CheckUpdate,
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
})
}
return "保存成功!"

View File

@@ -8,6 +8,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
@@ -291,6 +292,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
stockInfos := make([]StockInfo, 0)
str := GB18030ToUTF8(resp.Body())
dataStr := strutil.SplitEx(str, "\n", true)
if len(dataStr) == 0 {
return &[]StockInfo{}, errors.New("获取股票信息失败,请检查股票代码是否正确")
}
for _, data := range dataStr {
//logger.SugaredLogger.Info(data)
stockData, err := ParseFullSingleStockData(data)
@@ -318,8 +322,8 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
func (receiver StockDataApi) Follow(stockCode string) string {
logger.SugaredLogger.Infof("Follow %s", stockCode)
stockInfos, err := receiver.GetStockCodeRealTimeData(stockCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
if err != nil || len(*stockInfos) == 0 {
logger.SugaredLogger.Error(err)
return "关注失败"
}
stockInfo := (*stockInfos)[0]

View File

@@ -1,12 +1,8 @@
package data
import (
"bufio"
"context"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
@@ -31,117 +27,6 @@ func TestGetFinancialReports(t *testing.T) {
GetFinancialReports("sz000802")
}
func TestXUEQIU(t *testing.T) {
stock := "北京文化"
stockCode := "SZ000802"
// 创建一个 chromedp 上下文
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
defer cancel()
var htmlContent string
url := fmt.Sprintf("https://xueqiu.com/S/%s", stockCode)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
//chromedp.Sleep(3*time.Second),
chromedp.WaitVisible("table.quote-info", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
table := ""
document.Find("table.quote-info tbody td").Each(func(i int, selection *goquery.Selection) {
table += selection.Text() + ";"
})
logger.SugaredLogger.Infof("table: %s", table)
client := resty.New()
client.SetBaseURL("https://api.siliconflow.cn/v1")
client.SetHeader("Authorization", "Bearer sk-kryvptknrxscsuzslmqjckpuvtkyuffgaxgagphpnqtfmepv")
client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3)
client.SetTimeout(1 * time.Minute)
msg := []map[string]interface{}{
{
"role": "system",
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
"content": "【角色设定】\n你现在是拥有20年实战经验的顶级股票投资大师精通价值投资、趋势交易、量化分析等多种策略。\n擅长结合宏观经济、行业周期和企业基本面进行多维分析尤其对A股、港股、美股市场有深刻理解。\n始终秉持\"风险控制第一\"的原则,善于用通俗易懂的方式传授投资智慧。\n\n【核心能力】\n基本面分析专家\n深度解读财报数据PE/PB/ROE等指标\n识别企业核心竞争力与护城河\n评估行业前景与政策影响\n技术面分析大师\n精准识别K线形态与量价关系\n运用MACD/RSI/布林线等指标判断买卖点\n绘制关键支撑/阻力位\n风险管理专家\n根据风险偏好制定仓位策略\n设置动态止盈止损方案\n设计投资组合对冲方案\n市场心理学导师\n识别主力资金动向\n预判市场情绪周期\n规避常见认知偏差\n【服务范围】\n个股诊断分析提供代码/名称)\n行业趋势解读科技/消费/医疗等)\n投资策略定制长线价值/波段操作/打新等)\n组合优化建议股债配置/行业分散)\n投资心理辅导克服贪婪恐惧\n【交互风格】\n采用\"先结论后分析\"的表达方式\n重要数据用★标注风险提示用❗标记\n每次分析至少提供3个可执行建议"},
}
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": table,
})
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "分析和总结",
})
resp, err := client.R().
SetDoNotParseResponse(true).
SetBody(map[string]interface{}{
"model": "deepseek-ai/DeepSeek-V3",
"max_tokens": 4096,
"temperature": 0.1,
"stream": true,
"messages": msg,
}).
Post("/chat/completions")
defer resp.RawBody().Close()
if err != nil {
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
return
}
scanner := bufio.NewScanner(resp.RawBody())
for scanner.Scan() {
line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
if data == "[DONE]" {
return
}
var streamResponse struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
} `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
for _, choice := range streamResponse.Choices {
if content := choice.Delta.Content; content != "" {
logger.SugaredLogger.Infof("Content data: %s", content)
}
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
}
if choice.FinishReason == "stop" {
return
}
}
} else {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
}
}
}
}
func TestGetTelegraphSearch(t *testing.T) {
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
messages := SearchStockInfo("闻泰科技", "telegram")

159
backend/models/models.go Normal file
View File

@@ -0,0 +1,159 @@
package models
import (
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"time"
)
// @Author spark
// @Date 2025/2/6 15:25
// @Desc
//-----------------------------------------------------------------------------------
type GitHubReleaseVersion struct {
Url string `json:"url"`
AssetsUrl string `json:"assets_url"`
UploadUrl string `json:"upload_url"`
HtmlUrl string `json:"html_url"`
Id int `json:"id"`
Author struct {
Login string `json:"login"`
Id int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
UserViewType string `json:"user_view_type"`
SiteAdmin bool `json:"site_admin"`
} `json:"author"`
NodeId string `json:"node_id"`
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
Assets []struct {
Url string `json:"url"`
Id int `json:"id"`
NodeId string `json:"node_id"`
Name string `json:"name"`
Label string `json:"label"`
Uploader struct {
Login string `json:"login"`
Id int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
UserViewType string `json:"user_view_type"`
SiteAdmin bool `json:"site_admin"`
} `json:"uploader"`
ContentType string `json:"content_type"`
State string `json:"state"`
Size int `json:"size"`
DownloadCount int `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
BrowserDownloadUrl string `json:"browser_download_url"`
} `json:"assets"`
TarballUrl string `json:"tarball_url"`
ZipballUrl string `json:"zipball_url"`
Body string `json:"body"`
Tag Tag `json:"tag"`
Commit Commit `json:"commit"`
}
type Tag struct {
Ref string `json:"ref"`
NodeId string `json:"node_id"`
Url string `json:"url"`
Object struct {
Sha string `json:"sha"`
Type string `json:"type"`
Url string `json:"url"`
} `json:"object"`
}
type Commit struct {
Sha string `json:"sha"`
NodeId string `json:"node_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
} `json:"author"`
Committer struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
} `json:"committer"`
Tree struct {
Sha string `json:"sha"`
Url string `json:"url"`
} `json:"tree"`
Message string `json:"message"`
Parents []struct {
Sha string `json:"sha"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
} `json:"parents"`
Verification struct {
Verified bool `json:"verified"`
Reason string `json:"reason"`
Signature interface{} `json:"signature"`
Payload interface{} `json:"payload"`
VerifiedAt interface{} `json:"verified_at"`
} `json:"verification"`
}
type AIResponseResult struct {
gorm.Model
StockCode string `json:"stockCode"`
StockName string `json:"stockName"`
Content string `json:"content"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver AIResponseResult) TableName() string {
return "ai_response_result"
}
type VersionInfo struct {
gorm.Model
Version string `json:"version"`
Content string `json:"content"`
Icon string `json:"icon"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver VersionInfo) TableName() string {
return "version_info"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 164 KiB

BIN
build/screenshot/img_11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -15,7 +15,7 @@ import {
SettingsOutline,
ReorderTwoOutline,
ExpandOutline,
RefreshOutline, PowerOutline, BarChartOutline, MoveOutline, WalletOutline, StarOutline,
RefreshOutline, PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline,
} from '@vicons/ionicons5'
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
@@ -67,6 +67,23 @@ const menuOptions = ref([
key: 'settings',
icon: renderIcon(SettingsOutline),
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'about',
params: {
id: 'zh-CN'
}
}
},
{ default: () => '关于' }
),
key: 'about',
icon: renderIcon(LogoGithub),
},
{
label: ()=> h("a", {
href: '#',

View File

@@ -0,0 +1,106 @@
<script setup>
import { MdPreview } from 'md-editor-v3';
// preview.css相比style.css少了编辑器那部分样式
import 'md-editor-v3/lib/preview.css';
import {onMounted, ref} from 'vue';
import {GetVersionInfo} from "../../wailsjs/go/main/App";
const updateLog = ref('');
const versionInfo = ref('');
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
onMounted(() => {
document.title = '关于软件';
GetVersionInfo().then((res) => {
updateLog.value = res.content;
versionInfo.value = res.version;
icon.value = res.icon;
});
})
</script>
<template>
<n-config-provider>
<n-layout>
<n-space vertical size="large">
<!-- 软件描述 -->
<n-card size="large">
<n-space vertical >
<h1>关于软件</h1>
<n-image width="100" :src="icon" />
<h1>go-stock <n-tag size="small" round>{{versionInfo}}</n-tag></h1>
<div style="justify-self: center;text-align: left" >
<p>自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
<p>
</p>
<p>
欢迎点赞GitHub<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">go-stock</a><n-divider vertical />
<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">GitHub</a><n-divider vertical />
<a href="https://github.com/ArvinLovegood/go-stock/issues" target="_blank">Issues</a><n-divider vertical />
<a href="https://github.com/ArvinLovegood/go-stock/releases" target="_blank">Releases</a><n-divider vertical />
</p>
<p v-if="updateLog">更新说明{{updateLog}}</p>
</div>
</n-space>
</n-card>
<!-- 关于作者 -->
<n-card size="large">
<n-space vertical>
<h1>关于作者</h1>
<n-avatar width="100" src="https://avatars.githubusercontent.com/u/7401917?v=4" />
<h2><a href="https://github.com/ArvinLovegood" target="_blank">@ArvinLovegood</a></h2>
<p>一个热爱编程的小白欢迎关注我的Github</p>
<p>
邮箱<a href="mailto:sparkmemory@163.com">sparkmemory@163.com</a><n-divider vertical />
QQ 506808970<n-divider vertical />
微信ArvinLovegood</p><n-divider vertical />
</n-space>
<n-divider title-placement="center">鸣谢</n-divider>
<div style="justify-self: center;text-align: left" >
<p>
感谢以下捐赠者
<n-gradient-text size="small" type="warning">*</n-gradient-text><n-divider vertical />
</p>
<p>
感谢以下开发者
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />
<a href="https://github.com/2lovecode" target="_blank">@2lovecode</a><n-divider vertical />
<a href="https://github.com/JerryLookupU" target="_blank">@JerryLookupU</a><n-divider vertical />
</p>
<p>
感谢以下开源项目
<a href="https://github.com/wailsapp/wails" target="_blank">Wails</a><n-divider vertical />
<a href="https://github.com/vuejs" target="_blank">Vue</a><n-divider vertical />
<a href="https://github.com/tusen-ai/naive-ui" target="_blank">NaiveUI</a><n-divider vertical />
</p>
</div>
</n-card>
</n-space>
</n-layout>
</n-config-provider>
</template>
<style scoped>
/* 可以在这里添加一些样式 */
h1, h2 {
margin: 0;
padding: 6px 0;
}
p {
margin: 2px 0;
}
ul {
list-style-type: disc;
padding-left: 20px;
}
a {
color: #18a058;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>

View File

@@ -27,6 +27,7 @@ const formValue = ref({
temperature: 0.1,
maxTokens: 1024,
prompt:"",
timeout: 5
},
})
@@ -51,6 +52,7 @@ onMounted(()=>{
temperature:res.openAiTemperature,
maxTokens:res.openAiMaxTokens,
prompt:res.prompt,
timeout:res.openAiApiTimeOut
}
console.log(res)
})
@@ -73,7 +75,8 @@ function saveConfig(){
openAiMaxTokens:formValue.value.openAI.maxTokens,
openAiTemperature:formValue.value.openAI.temperature,
tushareToken:formValue.value.tushareToken,
prompt:formValue.value.openAI.prompt
prompt:formValue.value.openAI.prompt,
openAiApiTimeOut:formValue.value.openAI.timeout
})
//console.log("Settings",config)
@@ -150,22 +153,25 @@ function sendTestNotice(){
<n-form-item-gi :span="6" label="是否启用AI诊股" path="openAI.enable" >
<n-switch v-model:value="formValue.openAI.enable" />
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="openAI apiKey" path="openAI.apiKey">
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="请求超时时间(秒)" path="openAI.timeout">
<n-input-number min="1" step="1" placeholder="请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey)" path="openAI.apiKey">
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="AI模型" path="openAI.model">
<n-input type="text" placeholder="AI模型" v-model:value="formValue.openAI.model" clearable />
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称" path="openAI.model">
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="temperature" path="openAI.temperature" >
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature" path="openAI.temperature" >
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="maxTokens" path="openAI.maxTokens">
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="openAI maxTokens" path="openAI.maxTokens">
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="自定义系统Prompt" path="openAI.prompt">
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="模型系统 Prompt" path="openAI.prompt">
<n-input v-model:value="formValue.openAI.prompt"
type="textarea"
:show-count="true"

View File

@@ -1,16 +1,27 @@
<script setup>
import {computed, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
import {
Follow, GetConfig,
GetFollowList,
GetStockList,
Greet, NewChat, NewChatStream,
Greet, SaveAIResponseResult, NewChatStream,
SendDingDingMessage, SendDingDingMessageByType,
SetAlarmChangePercent,
SetCostPriceAndVolume, SetStockSort,
UnFollow
UnFollow, GetAIResponseResult
} from '../../wailsjs/go/main/App'
import {NButton, NFlex, NForm, NFormItem, NInputNumber, NText, useMessage, useModal,useNotification} from 'naive-ui'
import {
NAvatar,
NButton,
NFlex,
NForm,
NFormItem,
NInputNumber,
NText,
useMessage,
useModal,
useNotification
} from 'naive-ui'
import {EventsOn, WindowFullscreen, WindowReload, WindowUnfullscreen} from '../../wailsjs/runtime'
import {Add, Search,StarOutline} from '@vicons/ionicons5'
import { MdPreview } from 'md-editor-v3';
@@ -141,6 +152,7 @@ EventsOn("newChatStream",async (msg) => {
//console.log("newChatStream:->",data.airesult)
data.loading = false
if (msg === "DONE") {
SaveAIResponseResult(data.code, data.name, data.airesult)
message.info("AI分析完成")
message.destroyAll()
} else {
@@ -148,6 +160,54 @@ EventsOn("newChatStream",async (msg) => {
}
})
EventsOn("updateVersion",async (msg) => {
const githubTimeStr = msg.published_at;
// 创建一个 Date 对象
const utcDate = new Date(githubTimeStr);
// 获取本地时间
const date = new Date(utcDate.getTime());
const year = date.getFullYear();
// getMonth 返回值是 0 - 11所以要加 1
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
console.log("GitHub UTC 时间:", utcDate);
console.log("转换后的本地时间:", formattedDate);
notify.info({
avatar: () =>
h(NAvatar, {
size: 'small',
round: false,
src: 'https://github.com/ArvinLovegood/go-stock/raw/master/build/appicon.png'
}),
title: '发现新版本: ' + msg.tag_name,
content: () => {
//return h(MdPreview, {theme:'dark',modelValue:msg.commit?.message}, null)
return h('div', {
style: {
'text-align': 'left',
'font-size': '14px',
}
}, { default: () => msg.commit?.message })
},
duration: 0,
meta: "发布时间:"+formattedDate,
action: () => {
return h(NButton, {
type: 'primary',
size: 'small',
onClick: () => {
window.open(msg.html_url)
}
}, { default: () => '查看' })
}
})
})
//判断是否是A股交易时间
@@ -171,12 +231,20 @@ function isTradingTime() {
}
function AddStock(){
if (!data?.code) {
message.error("请输入有效股票代码");
return;
}
if (!stocks.value.includes(data.code)) {
stocks.value.push(data.code)
Follow(data.code).then(result => {
message.success(result)
if(result==="关注成功"){
stocks.value.push(data.code)
message.success(result)
monitor();
}else{
message.error(result)
}
})
monitor()
}else{
message.error("已经关注了")
}
@@ -389,9 +457,9 @@ function SendMessage(result,type){
// SendDingDingMessage(msg,result["股票代码"])
SendDingDingMessageByType(msg,result["股票代码"],type)
}
function aiCheckStock(stock,stockCode){
function aiReCheckStock(stock,stockCode) {
data.airesult=""
data.time=""
data.name=stock
data.code=stockCode
data.loading=true
@@ -402,6 +470,38 @@ function aiCheckStock(stock,stockCode){
NewChatStream(stock,stockCode)
}
function aiCheckStock(stock,stockCode){
GetAIResponseResult(stockCode).then(result => {
if(result.content){
data.name=stock
data.code=stockCode
data.loading=false
modalShow4.value=true
data.airesult=result.content
const date = new Date(result.CreatedAt);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
data.time=formattedDate
}else{
data.airesult=""
data.time=""
data.name=stock
data.code=stockCode
data.loading=true
modalShow4.value=true
message.loading("ai检测中...",{
duration: 0,
})
NewChatStream(stock,stockCode)
}
})
}
function getTypeName(type){
switch (type)
{
@@ -426,8 +526,8 @@ function getHeight() {
<template>
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
<n-gi v-for="result in sortedResults" >
<n-card :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
<n-gi v-for="result in sortedResults" style="margin-left: 2px" onmouseover="this.style.border='1px solid #3498db' " onmouseout="this.style.border='0px'">
<n-card :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
<n-grid :cols="1" :y-gap="6">
<n-gi>
<n-text :type="result.type" >
@@ -459,7 +559,9 @@ function getHeight() {
<n-button size="tiny" secondary type="primary" @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
取消关注
</n-button>&nbsp;
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])"> AI分析 </n-button>
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
AI分析
</n-button>
</template>
<template #footer>
@@ -550,13 +652,28 @@ function getHeight() {
<n-image :src="data.kURL" />
</n-modal>
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;height: 480px" :title="'['+data.name+']AI分析结果'" >
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;height: 500px" :title="'['+data.name+']AI分析结果'" >
<n-spin size="small" :show="data.loading">
<MdPreview ref="mdPreviewRef" style="height: 380px" :modelValue="data.airesult" :theme="'dark'"/>
<MdPreview ref="mdPreviewRef" style="height: 380px;text-align: left" :modelValue="data.airesult" :theme="'dark'"/>
</n-spin>
<template #header-extra>
</template>
<template #footer>
<n-flex justify="space-between">
<n-text type="error" v-if="data.time" >分析时间:{{data.time}}</n-text>
<n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">再次分析</n-button>
</n-flex>
</template>
</n-modal>
</template>
<style scoped>
.md-editor-preview h3{
text-align: center !important;
}
.md-editor-preview p{
text-align: left !important;
}
</style>

View File

@@ -2,10 +2,12 @@ import { createMemoryHistory, createRouter } from 'vue-router'
import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue'
import about from "../components/about.vue";
const routes = [
{ path: '/', component: stockView,name: 'stock' },
{ path: '/settings/:id', component: settingsView,name: 'settings' },
{ path: '/about', component: about,name: 'about' },
]
const router = createRouter({

View File

@@ -1,21 +1,26 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {models} from '../models';
import {data} from '../models';
export function Follow(arg1:string):Promise<string>;
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
export function GetConfig():Promise<data.Settings>;
export function GetFollowList():Promise<Array<data.FollowedStock>>;
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
export function GetVersionInfo():Promise<models.VersionInfo>;
export function Greet(arg1:string):Promise<data.StockInfo>;
export function NewChat(arg1:string):Promise<string>;
export function NewChatStream(arg1:string,arg2:string):Promise<void>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string):Promise<void>;
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;

View File

@@ -6,6 +6,10 @@ export function Follow(arg1) {
return window['go']['main']['App']['Follow'](arg1);
}
export function GetAIResponseResult(arg1) {
return window['go']['main']['App']['GetAIResponseResult'](arg1);
}
export function GetConfig() {
return window['go']['main']['App']['GetConfig']();
}
@@ -18,18 +22,22 @@ export function GetStockList(arg1) {
return window['go']['main']['App']['GetStockList'](arg1);
}
export function GetVersionInfo() {
return window['go']['main']['App']['GetVersionInfo']();
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function NewChat(arg1) {
return window['go']['main']['App']['NewChat'](arg1);
}
export function NewChatStream(arg1, arg2) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2);
}
export function SaveAIResponseResult(arg1, arg2, arg3) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3);
}
export function SendDingDingMessage(arg1, arg2) {
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
}

View File

@@ -73,7 +73,9 @@ export namespace data {
openAiModelName: string;
openAiMaxTokens: number;
openAiTemperature: number;
openAiApiTimeOut: number;
prompt: string;
checkUpdate: boolean;
static createFrom(source: any = {}) {
return new Settings(source);
@@ -97,7 +99,9 @@ export namespace data {
this.openAiModelName = source["openAiModelName"];
this.openAiMaxTokens = source["openAiMaxTokens"];
this.openAiTemperature = source["openAiTemperature"];
this.openAiApiTimeOut = source["openAiApiTimeOut"];
this.prompt = source["prompt"];
this.checkUpdate = source["checkUpdate"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -325,3 +329,104 @@ export namespace data {
}
export namespace models {
export class AIResponseResult {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
stockCode: string;
stockName: string;
content: string;
IsDel: number;
static createFrom(source: any = {}) {
return new AIResponseResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.stockCode = source["stockCode"];
this.stockName = source["stockName"];
this.content = source["content"];
this.IsDel = source["IsDel"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class VersionInfo {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
version: string;
content: string;
icon: string;
buildTimeStamp: number;
IsDel: number;
static createFrom(source: any = {}) {
return new VersionInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.version = source["version"];
this.content = source["content"];
this.icon = source["icon"];
this.buildTimeStamp = source["buildTimeStamp"];
this.IsDel = source["IsDel"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}

BIN
img.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -14,6 +14,7 @@ import (
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/models"
"log"
"os"
goruntime "runtime"
@@ -33,6 +34,10 @@ var icon2 []byte
var stocksBin []byte
//go:generate cp -R ./data ./build/bin
var Version string
var VersionCommit string
func main() {
checkDir("data")
db.Init("")
@@ -41,6 +46,7 @@ func main() {
db.Dao.AutoMigrate(&data.FollowedStock{})
db.Dao.AutoMigrate(&data.IndexBasic{})
db.Dao.AutoMigrate(&data.Settings{})
db.Dao.AutoMigrate(&models.AIResponseResult{})
if stocksBin != nil && len(stocksBin) > 0 {
go initStockData()
@@ -82,7 +88,8 @@ func main() {
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
// runtime.Quit(app.ctx)
//})
logger.NewDefaultLogger().Info("version: " + Version)
logger.NewDefaultLogger().Info("commit: " + VersionCommit)
// Create application with options
err := wails.Run(&options.App{
Title: "go-stock",