Compare commits
41 Commits
v2025.7.8.
...
v2025.7.14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
490a3c0847 | ||
|
|
38f83674ef | ||
|
|
d26c4bc986 | ||
|
|
7e919376b5 | ||
|
|
1d9ef724e6 | ||
|
|
8e982d4430 | ||
|
|
a67559831a | ||
|
|
9718d3311d | ||
|
|
789e7427ce | ||
|
|
801aa14c7a | ||
|
|
f5c621fbcc | ||
|
|
119f0f8aa7 | ||
|
|
fe814974fd | ||
|
|
dd3c231637 | ||
|
|
e05ff94aba | ||
|
|
bbd4bb5b48 | ||
|
|
58f3009902 | ||
|
|
c6b841fb8f | ||
|
|
2b28390414 | ||
|
|
7887dfed5e | ||
|
|
a4c98933a4 | ||
|
|
ad63ffff7f | ||
|
|
1ccc2f8b1f | ||
|
|
dc5483aa07 | ||
|
|
8c82ba4a38 | ||
|
|
fd905ff278 | ||
|
|
6ec0f5fbe0 | ||
|
|
32706fb4dc | ||
|
|
2cb661734f | ||
|
|
4fab910340 | ||
|
|
84e4ba8474 | ||
|
|
76a44fae32 | ||
|
|
7ea974f1a6 | ||
|
|
7ea160b6b5 | ||
|
|
c2f260c613 | ||
|
|
2d224ccfc4 | ||
|
|
a66f2156f1 | ||
|
|
e90727773f | ||
|
|
89dcb713be | ||
|
|
6f4b21207d | ||
|
|
f51e3d863a |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -11,6 +11,7 @@ env:
|
||||
# Necessary for most environments as build failure can occur due to OOM issues
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
OFFICIAL_STATEMENT: ${{ vars.OFFICIAL_STATEMENT }}
|
||||
BUILD_KEY: ${{ vars.BUILD_KEY }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -53,4 +54,5 @@ jobs:
|
||||
build-tags: ${{ github.ref_name }}
|
||||
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
|
||||
build-statement: ${{ env.OFFICIAL_STATEMENT }}
|
||||
build-key: ${{ env.BUILD_KEY }}
|
||||
node-version: '20.x'
|
||||
|
||||
10
README.md
10
README.md
@@ -23,6 +23,7 @@
|
||||
### 📦 立即体验
|
||||
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- MACOS版:[go-stock-darwin-universal](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
|
||||
|
||||
### 💬 支持大模型/平台
|
||||
@@ -44,6 +45,14 @@
|
||||
- 欢迎大家提出宝贵的建议,欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
|
||||
|
||||
|
||||
### 支持开源💕计划
|
||||
| 赞助计划 | 赞助等级 | 权益说明 |
|
||||
|:--------------------------------|----------------|:-------------------------------------------------------|
|
||||
| 每月 0 RMB | vip0 | 🌟 全部功能,软件自动更新(从GitHub下载),自行解决github平台网络问题。 |
|
||||
| 每月赞助 18.8 RMB<br>每年赞助 120 RMB | vip1 | 💕 全部功能,软件自动更新(从CDN下载),更新快速便捷。AI配置指导,提示词参考等 |
|
||||
| 每月赞助 28.8 RMB<br>每年赞助 240 RMB | vip2 | 💕 💕 vip1全部功能,赠送硅基流动AI分析服务 |
|
||||
| 每月赞助 X RMB | vipX | 🧩 更多计划,视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖) |
|
||||
|
||||
## 🧩 重大功能开发计划
|
||||
| 功能说明 | 状态 | 备注 |
|
||||
|-----------------|----|----------------------------------------------------------------------------------------------------------|
|
||||
@@ -57,6 +66,7 @@
|
||||
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
|
||||
|
||||
## 👀 更新日志
|
||||
### 2025.07.08 实现软件自动更新功能
|
||||
### 2025.07.07 卡片添加迷你分时图
|
||||
### 2025.07.05 MacOs支持
|
||||
### 2025.07.01 AI分析集成工具函数,AI分析将更加智能
|
||||
|
||||
268
app.go
268
app.go
@@ -1,14 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/cryptor"
|
||||
"github.com/inconshreveable/go-update"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,11 +31,12 @@ import (
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
cron *cron.Cron
|
||||
cronEntrys map[string]cron.EntryID
|
||||
AiTools []data.Tool
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
cron *cron.Cron
|
||||
cronEntrys map[string]cron.EntryID
|
||||
AiTools []data.Tool
|
||||
SponsorInfo map[string]any
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
@@ -100,7 +107,64 @@ func AddTools(tools []data.Tool) []data.Tool {
|
||||
return tools
|
||||
}
|
||||
|
||||
func (a *App) GetSponsorInfo() map[string]any {
|
||||
return a.SponsorInfo
|
||||
}
|
||||
func (a *App) CheckSponsorCode(sponsorCode string) map[string]any {
|
||||
sponsorCode = strutil.Trim(sponsorCode)
|
||||
if sponsorCode != "" {
|
||||
encrypted, err := hex.DecodeString(sponsorCode)
|
||||
if err != nil {
|
||||
return map[string]any{
|
||||
"code": 0,
|
||||
"msg": "赞助码格式错误,请输入正确的赞助码!",
|
||||
}
|
||||
}
|
||||
key, err := hex.DecodeString(BuildKey)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return map[string]any{
|
||||
"code": 0,
|
||||
"msg": "版本错误,不支持赞助码!",
|
||||
}
|
||||
}
|
||||
decrypt := cryptor.AesEcbDecrypt(encrypted, key)
|
||||
if decrypt == nil || len(decrypt) == 0 {
|
||||
return map[string]any{
|
||||
"code": 0,
|
||||
"msg": "赞助码错误,请输入正确的赞助码!",
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"code": 1,
|
||||
"msg": "赞助码校验成功,感谢您的支持!",
|
||||
}
|
||||
} else {
|
||||
return map[string]any{"code": 0, "message": "赞助码不能为空,请输入正确的赞助码!"}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) CheckUpdate() {
|
||||
sponsorCode := strutil.Trim(a.GetConfig().SponsorCode)
|
||||
if sponsorCode != "" {
|
||||
encrypted, err := hex.DecodeString(sponsorCode)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
key, err := hex.DecodeString(BuildKey)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
decrypt := string(cryptor.AesEcbDecrypt(encrypted, key))
|
||||
err = json.Unmarshal([]byte(decrypt), &a.SponsorInfo)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
releaseVersion := &models.GitHubReleaseVersion{}
|
||||
_, err := resty.New().R().
|
||||
SetResult(releaseVersion).
|
||||
@@ -111,6 +175,7 @@ func (a *App) CheckUpdate() {
|
||||
}
|
||||
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
|
||||
if releaseVersion.TagName != Version {
|
||||
|
||||
tag := &models.Tag{}
|
||||
_, err = resty.New().R().
|
||||
SetResult(tag).
|
||||
@@ -118,6 +183,7 @@ func (a *App) CheckUpdate() {
|
||||
if err == nil {
|
||||
releaseVersion.Tag = *tag
|
||||
}
|
||||
|
||||
commit := &models.Commit{}
|
||||
_, err = resty.New().R().
|
||||
SetResult(commit).
|
||||
@@ -126,7 +192,109 @@ func (a *App) CheckUpdate() {
|
||||
releaseVersion.Commit = *commit
|
||||
}
|
||||
|
||||
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
||||
if !(IsWindows() || IsMacOS()) {
|
||||
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
||||
return
|
||||
}
|
||||
downloadUrl := fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
|
||||
if IsMacOS() {
|
||||
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
|
||||
}
|
||||
sponsorCode = strutil.Trim(a.GetConfig().SponsorCode)
|
||||
if sponsorCode != "" {
|
||||
encrypted, err := hex.DecodeString(sponsorCode)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
key, err := hex.DecodeString(BuildKey)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
decrypt := string(cryptor.AesEcbDecrypt(encrypted, key))
|
||||
err = json.Unmarshal([]byte(decrypt), &a.SponsorInfo)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
vipStartTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipStartTime"].(string), time.Local)
|
||||
vipEndTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipEndTime"].(string), time.Local)
|
||||
vipAuthTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipAuthTime"].(string), time.Local)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
isVip := false
|
||||
|
||||
if time.Now().After(vipAuthTime) && time.Now().After(vipStartTime) && time.Now().Before(vipEndTime) {
|
||||
isVip = true
|
||||
}
|
||||
|
||||
if IsWindows() {
|
||||
if isVip {
|
||||
if a.SponsorInfo["winDownUrl"] == nil {
|
||||
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
|
||||
} else {
|
||||
downloadUrl = a.SponsorInfo["winDownUrl"].(string)
|
||||
}
|
||||
} else {
|
||||
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
|
||||
}
|
||||
}
|
||||
if IsMacOS() {
|
||||
if isVip {
|
||||
if a.SponsorInfo["macDownUrl"] == nil {
|
||||
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
|
||||
} else {
|
||||
downloadUrl = a.SponsorInfo["macDownUrl"].(string)
|
||||
}
|
||||
} else {
|
||||
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
|
||||
}
|
||||
}
|
||||
}
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
|
||||
"time": "发现新版本:" + releaseVersion.TagName,
|
||||
"isRed": false,
|
||||
"source": "go-stock",
|
||||
"content": fmt.Sprintf("当前版本:%s, 最新版本:%s,后台开始下载...\n%s", Version, releaseVersion.TagName, commit.Message),
|
||||
})
|
||||
resp, err := resty.New().R().Get(downloadUrl)
|
||||
if err != nil {
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
|
||||
"time": "新版本:" + releaseVersion.TagName,
|
||||
"isRed": true,
|
||||
"source": "go-stock",
|
||||
"content": commit.Message + "\n新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
|
||||
})
|
||||
return
|
||||
}
|
||||
body := resp.Body()
|
||||
|
||||
if len(body) < 1024 {
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
|
||||
"time": "新版本:" + releaseVersion.TagName,
|
||||
"isRed": true,
|
||||
"source": "go-stock",
|
||||
"content": commit.Message + "\n新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = update.Apply(bytes.NewReader(body), update.Options{})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error("更新失败: ", err.Error())
|
||||
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
||||
return
|
||||
} else {
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
|
||||
"time": "新版本:" + releaseVersion.TagName,
|
||||
"isRed": true,
|
||||
"source": "go-stock",
|
||||
"content": "版本更新完成,下次重启软件生效.",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,6 +864,7 @@ func (a *App) GetVersionInfo() *models.VersionInfo {
|
||||
Icon: GetImageBase(icon),
|
||||
Alipay: GetImageBase(alipay),
|
||||
Wxpay: GetImageBase(wxpay),
|
||||
Wxgzh: GetImageBase(wxgzh),
|
||||
Content: VersionCommit,
|
||||
OfficialStatement: OFFICIAL_STATEMENT,
|
||||
}
|
||||
@@ -829,15 +998,6 @@ func (a *App) ExportConfig() string {
|
||||
}
|
||||
return "导出成功:" + file
|
||||
}
|
||||
func getScreenResolution() (int, int, error) {
|
||||
//user32 := syscall.NewLazyDLL("user32.dll")
|
||||
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
//
|
||||
//width, _, _ := getSystemMetrics.Call(0)
|
||||
//height, _, _ := getSystemMetrics.Call(1)
|
||||
|
||||
return int(1366), int(768), nil
|
||||
}
|
||||
|
||||
func (a *App) ShareAnalysis(stockCode, stockName string) string {
|
||||
//http://go-stock.sparkmemory.top:16688/upload
|
||||
@@ -1033,3 +1193,81 @@ func (a *App) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]a
|
||||
slice.Reverse(res)
|
||||
return res
|
||||
}
|
||||
|
||||
// OpenURL
|
||||
//
|
||||
// @Description: 跨平台打开默认浏览器
|
||||
// @receiver a
|
||||
// @param url
|
||||
func (a *App) OpenURL(url string) {
|
||||
runtime.BrowserOpenURL(a.ctx, url)
|
||||
}
|
||||
|
||||
// SaveImage
|
||||
//
|
||||
// @Description: 跨平台保存图片
|
||||
// @receiver a
|
||||
// @param name
|
||||
// @param base64Data
|
||||
// @return error
|
||||
func (a *App) SaveImage(name, base64Data string) string {
|
||||
// 打开保存文件对话框
|
||||
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
Title: "保存图片",
|
||||
DefaultFilename: name + "AI分析.png",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "PNG 图片",
|
||||
Pattern: "*.png",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil || filePath == "" {
|
||||
return "文件路径,无法保存。"
|
||||
}
|
||||
|
||||
// 解码并保存
|
||||
decodeString, err := base64.StdEncoding.DecodeString(base64Data)
|
||||
if err != nil {
|
||||
return "文件内容异常,无法保存。"
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Clean(filePath), decodeString, 0777)
|
||||
if err != nil {
|
||||
return "保存结果异常,无法保存。"
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
// SaveWordFile
|
||||
//
|
||||
// @Description: // 跨平台保存word
|
||||
// @receiver a
|
||||
// @param filename
|
||||
// @param base64Data
|
||||
// @return error
|
||||
func (a *App) SaveWordFile(filename string, base64Data string) string {
|
||||
// 弹出保存文件对话框
|
||||
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
Title: "保存 Word 文件",
|
||||
DefaultFilename: filename,
|
||||
Filters: []runtime.FileFilter{
|
||||
{DisplayName: "Word 文件", Pattern: "*.docx"},
|
||||
},
|
||||
})
|
||||
if err != nil || filePath == "" {
|
||||
return "文件路径,无法保存。"
|
||||
}
|
||||
|
||||
// 解码 base64 内容
|
||||
decodeString, err := base64.StdEncoding.DecodeString(base64Data)
|
||||
if err != nil {
|
||||
return "文件内容异常,无法保存。"
|
||||
}
|
||||
// 保存为文件
|
||||
err = os.WriteFile(filepath.Clean(filePath), decodeString, 0777)
|
||||
if err != nil {
|
||||
return "保存结果异常,无法保存。"
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
@@ -67,9 +67,31 @@ func (a *App) startup(ctx context.Context) {
|
||||
log.Fatalf("系统通知失败: %v", err)
|
||||
}
|
||||
}()
|
||||
go setUpScreen(a)
|
||||
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
|
||||
}
|
||||
|
||||
func setUpScreen(a *App) {
|
||||
screens, _ := runtime.ScreenGetAll(a.ctx)
|
||||
if len(screens) == 0 {
|
||||
return
|
||||
}
|
||||
screen := screens[0]
|
||||
sw, sh := screen.Width, screen.Height
|
||||
|
||||
// macOS 菜单栏 + Dock 留出空间
|
||||
topBarHeight := 22
|
||||
dockHeight := 56
|
||||
verticalMargin := topBarHeight + dockHeight
|
||||
|
||||
// 设置窗口为屏幕 80% 宽 × 可用高度 90%
|
||||
w := int(float64(sw) * 0.8)
|
||||
h := int(float64(sh-verticalMargin) * 0.9)
|
||||
|
||||
runtime.WindowSetSize(a.ctx, w, h)
|
||||
runtime.WindowCenter(a.ctx)
|
||||
}
|
||||
|
||||
// OnSecondInstanceLaunch 处理第二实例启动时的通知
|
||||
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
err := beeep.Notify("go-stock", "程序已经在运行了", "")
|
||||
@@ -166,3 +188,17 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
return false // 如果选择了确定,继续关闭应用
|
||||
}
|
||||
}
|
||||
|
||||
func getFrameless() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func getScreenResolution() (int, int, int, int, error) {
|
||||
//user32 := syscall.NewLazyDLL("user32.dll")
|
||||
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
//
|
||||
//width, _, _ := getSystemMetrics.Call(0)
|
||||
//height, _, _ := getSystemMetrics.Call(1)
|
||||
|
||||
return int(1200), int(800), 0, 0, nil
|
||||
}
|
||||
|
||||
@@ -199,3 +199,17 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func getFrameless() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func getScreenResolution() (int, int, int, int, error) {
|
||||
//user32 := syscall.NewLazyDLL("user32.dll")
|
||||
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
//
|
||||
//width, _, _ := getSystemMetrics.Call(0)
|
||||
//height, _, _ := getSystemMetrics.Call(1)
|
||||
|
||||
return int(1366), int(768), 1456, 768, nil
|
||||
}
|
||||
|
||||
@@ -701,3 +701,134 @@ func (m MarketNewsApi) ClsCalendar() []any {
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetGDP() *models.GDPResp {
|
||||
res := &models.GDPResp{}
|
||||
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CDOMESTICL_PRODUCT_BASE%2CFIRST_PRODUCT_BASE%2CSECOND_PRODUCT_BASE%2CTHIRD_PRODUCT_BASE%2CSUM_SAME%2CFIRST_SAME%2CSECOND_SAME%2CTHIRD_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_GDP&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
SetHeader("Origin", "https://datacenter.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
logger.SugaredLogger.Debugf("GDP:%s", body)
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
logger.SugaredLogger.Infof("GDP:%v", data)
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
json.Unmarshal(marshal, &res)
|
||||
logger.SugaredLogger.Infof("GDP:%+v", res)
|
||||
return res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetCPI() *models.CPIResp {
|
||||
res := &models.CPIResp{}
|
||||
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CNATIONAL_SAME%2CNATIONAL_BASE%2CNATIONAL_SEQUENTIAL%2CNATIONAL_ACCUMULATE%2CCITY_SAME%2CCITY_BASE%2CCITY_SEQUENTIAL%2CCITY_ACCUMULATE%2CRURAL_SAME%2CRURAL_BASE%2CRURAL_SEQUENTIAL%2CRURAL_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_CPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
SetHeader("Origin", "https://datacenter.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
logger.SugaredLogger.Debugf("GDP:%s", body)
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
logger.SugaredLogger.Infof("GDP:%v", data)
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
json.Unmarshal(marshal, &res)
|
||||
logger.SugaredLogger.Infof("GDP:%+v", res)
|
||||
return res
|
||||
}
|
||||
|
||||
// GetPPI PPI
|
||||
func (m MarketNewsApi) GetPPI() *models.PPIResp {
|
||||
res := &models.PPIResp{}
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE,TIME,BASE,BASE_SAME,BASE_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
SetHeader("Origin", "https://datacenter.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
json.Unmarshal(marshal, &res)
|
||||
return res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetPMI() *models.PMIResp {
|
||||
res := &models.PMIResp{}
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CMAKE_INDEX%2CMAKE_SAME%2CNMAKE_INDEX%2CNMAKE_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PMI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
SetHeader("Origin", "https://datacenter.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
body := resp.Body()
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
json.Unmarshal(marshal, &res)
|
||||
return res
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/util"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -175,3 +176,28 @@ func TestClsCalendar(t *testing.T) {
|
||||
}
|
||||
logger.SugaredLogger.Debugf("md:\n %s", md.String())
|
||||
}
|
||||
|
||||
func TestGetGDP(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetGDP()
|
||||
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
func TestGetCPI(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetCPI()
|
||||
md := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res.CPIResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
|
||||
// PPI
|
||||
func TestGetPPI(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetPPI()
|
||||
md := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res.PPIResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
|
||||
// PMI
|
||||
func TestGetPMI(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetPMI()
|
||||
md := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res.PMIResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -41,6 +42,11 @@ type OpenAi struct {
|
||||
BrowserPath string `json:"browser_path"`
|
||||
}
|
||||
|
||||
func (o OpenAi) String() string {
|
||||
return fmt.Sprintf("OpenAi{BaseUrl: %s, Model: %s, MaxTokens: %d, Temperature: %.2f, Prompt: %s, TimeOut: %d, QuestionTemplate: %s, CrawlTimeOut: %d, KDays: %d, BrowserPath: %s, ApiKey: [MASKED]}",
|
||||
o.BaseUrl, o.Model, o.MaxTokens, o.Temperature, o.Prompt, o.TimeOut, o.QuestionTemplate, o.CrawlTimeOut, o.KDays, o.BrowserPath)
|
||||
}
|
||||
|
||||
func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
|
||||
config := GetConfig()
|
||||
if config.OpenAiEnable {
|
||||
@@ -140,8 +146,8 @@ func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromp
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic: %s", err)
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config: %s", o.String())
|
||||
}
|
||||
}()
|
||||
defer close(ch)
|
||||
@@ -173,7 +179,34 @@ func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromp
|
||||
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
wg.Add(3)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
res := NewMarketNewsApi().GetGDP()
|
||||
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
|
||||
market.WriteString(md)
|
||||
res2 := NewMarketNewsApi().GetCPI()
|
||||
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
|
||||
market.WriteString(md2)
|
||||
res3 := NewMarketNewsApi().GetPPI()
|
||||
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
|
||||
market.WriteString(md3)
|
||||
res4 := NewMarketNewsApi().GetPMI()
|
||||
md4 := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res4.PMIResult.Data)
|
||||
market.WriteString(md4)
|
||||
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "国内宏观经济数据",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "\n# 国内宏观经济数据:\n" + market.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
@@ -264,7 +297,7 @@ func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int)
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%s", o.String())
|
||||
}
|
||||
}()
|
||||
defer close(ch)
|
||||
@@ -356,7 +389,7 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Errorf("NewChatStream goroutine panic :%s", err)
|
||||
logger.SugaredLogger.Errorf("NewChatStream goroutine panic stock:%s stockCode:%s", stock, stockCode)
|
||||
logger.SugaredLogger.Errorf("NewChatStream goroutine panic config:%v", o)
|
||||
logger.SugaredLogger.Errorf("NewChatStream goroutine panic config:%s", o.String())
|
||||
}
|
||||
}()
|
||||
defer close(ch)
|
||||
|
||||
@@ -35,6 +35,7 @@ type Settings struct {
|
||||
BrowserPoolSize int `json:"browserPoolSize"`
|
||||
EnableFund bool `json:"enableFund"`
|
||||
EnablePushNews bool `json:"enablePushNews"`
|
||||
SponsorCode string `json:"sponsorCode"`
|
||||
}
|
||||
|
||||
func (receiver Settings) TableName() string {
|
||||
@@ -80,6 +81,7 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
"dark_theme": s.Config.DarkTheme,
|
||||
"enable_fund": s.Config.EnableFund,
|
||||
"enable_push_news": s.Config.EnablePushNews,
|
||||
"sponsor_code": s.Config.SponsorCode,
|
||||
})
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
|
||||
@@ -108,6 +110,7 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
DarkTheme: s.Config.DarkTheme,
|
||||
EnableFund: s.Config.EnableFund,
|
||||
EnablePushNews: s.Config.EnablePushNews,
|
||||
SponsorCode: s.Config.SponsorCode,
|
||||
})
|
||||
}
|
||||
return "保存成功!"
|
||||
|
||||
@@ -155,6 +155,7 @@ type VersionInfo struct {
|
||||
Icon string `json:"icon"`
|
||||
Alipay string `json:"alipay"`
|
||||
Wxpay string `json:"wxpay"`
|
||||
Wxgzh string `json:"wxgzh"`
|
||||
BuildTimeStamp int64 `json:"buildTimeStamp"`
|
||||
OfficialStatement string `json:"officialStatement"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
@@ -369,3 +370,94 @@ type HotEvent struct {
|
||||
StatusCount int `json:"status_count"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type GDP struct {
|
||||
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
|
||||
TIME string `json:"TIME" md:"报告期"`
|
||||
DOMESTICLPRODUCTBASE float64 `json:"DOMESTICL_PRODUCT_BASE" md:"国内生产总值(亿元)"`
|
||||
SUMSAME float64 `json:"SUM_SAME" md:"国内生产总值同比增长(%)"`
|
||||
FIRSTPRODUCTBASE float64 `json:"FIRST_PRODUCT_BASE" md:"第一产业(亿元)"`
|
||||
FIRSTSAME int `json:"FIRST_SAME" md:"第一产业同比增长(%)"`
|
||||
SECONDPRODUCTBASE float64 `json:"SECOND_PRODUCT_BASE" md:"第二产业(亿元)"`
|
||||
SECONDSAME float64 `json:"SECOND_SAME" md:"第二产业同比增长(%)"`
|
||||
THIRDPRODUCTBASE float64 `json:"THIRD_PRODUCT_BASE" md:"第三产业(亿元)"`
|
||||
THIRDSAME float64 `json:"THIRD_SAME" md:"第三产业同比增长(%)"`
|
||||
}
|
||||
type CPI struct {
|
||||
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
|
||||
TIME string `json:"TIME" md:"报告期"`
|
||||
NATIONALBASE float64 `json:"NATIONAL_BASE" md:"全国当月"`
|
||||
NATIONALSAME float64 `json:"NATIONAL_SAME" md:"全国当月同比增长(%)"`
|
||||
NATIONALSEQUENTIAL float64 `json:"NATIONAL_SEQUENTIAL" md:"全国当月环比增长(%)"`
|
||||
NATIONALACCUMULATE float64 `json:"NATIONAL_ACCUMULATE" md:"全国当月累计"`
|
||||
CITYBASE float64 `json:"CITY_BASE" md:"城市当月"`
|
||||
CITYSAME float64 `json:"CITY_SAME" md:"城市当月同比增长(%)"`
|
||||
CITYSEQUENTIAL float64 `json:"CITY_SEQUENTIAL" md:"城市当月环比增长(%)"`
|
||||
CITYACCUMULATE int `json:"CITY_ACCUMULATE" md:"城市当月累计"`
|
||||
RURALBASE float64 `json:"RURAL_BASE" md:"农村当月"`
|
||||
RURALSAME float64 `json:"RURAL_SAME" md:"农村当月同比增长(%)"`
|
||||
RURALSEQUENTIAL int `json:"RURAL_SEQUENTIAL" md:"农村当月环比增长(%)"`
|
||||
RURALACCUMULATE float64 `json:"RURAL_ACCUMULATE" md:"农村当月累计"`
|
||||
}
|
||||
type PPI struct {
|
||||
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
|
||||
TIME string `json:"TIME" md:"报告期"`
|
||||
BASE float64 `json:"BASE" md:"当月"`
|
||||
BASESAME float64 `json:"BASE_SAME" md:"当月同比增长(%)"`
|
||||
BASEACCUMULATE float64 `json:"BASE_ACCUMULATE" md:"累计"`
|
||||
}
|
||||
type PMI struct {
|
||||
REPORTDATE string `md:"报告时间" json:"REPORT_DATE"`
|
||||
TIME string `md:"报告期" json:"TIME"`
|
||||
MAKEINDEX float64 `md:"制造业指数" json:"MAKE_INDEX"`
|
||||
MAKESAME float64 `md:"制造业指数同比增长(%)" json:"MAKE_SAME"`
|
||||
NMAKEINDEX float64 `md:"非制造业" json:"NMAKE_INDEX"`
|
||||
NMAKESAME float64 `md:"非制造业同比增长(%)" json:"NMAKE_SAME"`
|
||||
}
|
||||
|
||||
type DCResp struct {
|
||||
Version string `json:"version"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
type GDPResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []GDP `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
type CPIResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []CPI `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type PPIResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []PPI `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
type PMIResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []PMI `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
type GDPResp struct {
|
||||
DCResp
|
||||
GDPResult GDPResult `json:"result"`
|
||||
}
|
||||
|
||||
type CPIResp struct {
|
||||
DCResp
|
||||
CPIResult CPIResult `json:"result"`
|
||||
}
|
||||
|
||||
type PPIResp struct {
|
||||
DCResp
|
||||
PPIResult PPIResult `json:"result"`
|
||||
}
|
||||
type PMIResp struct {
|
||||
DCResp
|
||||
PMIResult PMIResult `json:"result"`
|
||||
}
|
||||
|
||||
284
backend/util/struct_to_markdown.go
Normal file
284
backend/util/struct_to_markdown.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MarkdownTable 生成结构体或结构体切片的Markdown表格表示
|
||||
func MarkdownTable(v interface{}) string {
|
||||
value := reflect.ValueOf(v)
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
// 处理单个结构体
|
||||
if value.Kind() == reflect.Struct {
|
||||
return markdownSingleStruct(value)
|
||||
}
|
||||
|
||||
// 处理结构体切片/数组
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
|
||||
if value.Len() == 0 {
|
||||
return "切片/数组为空"
|
||||
}
|
||||
return markdownStructSlice(value)
|
||||
}
|
||||
|
||||
return "输入必须是结构体、结构体指针、结构体切片或数组"
|
||||
}
|
||||
|
||||
func MarkdownTableWithTitle(title string, v interface{}) string {
|
||||
value := reflect.ValueOf(v)
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
// 处理单个结构体
|
||||
if value.Kind() == reflect.Struct {
|
||||
return markdownSingleStruct(value)
|
||||
}
|
||||
|
||||
// 处理结构体切片/数组
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
|
||||
if value.Len() == 0 {
|
||||
return "\n## " + title + "\n" + "无数据" + "\n"
|
||||
}
|
||||
return "\n## " + title + "\n" + markdownStructSlice(value) + "\n"
|
||||
}
|
||||
|
||||
return "\n## " + title + "\n" + "无数据" + "\n"
|
||||
}
|
||||
|
||||
// 处理单个结构体
|
||||
func markdownSingleStruct(value reflect.Value) string {
|
||||
t := value.Type()
|
||||
var b strings.Builder
|
||||
|
||||
// 表头
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s |", getFieldName(field)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// 分隔线
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(" --- |")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// 数据行
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
fieldValue := value.Field(i)
|
||||
b.WriteString(fmt.Sprintf(" %s |", formatValue(fieldValue)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// 处理结构体切片/数组
|
||||
func markdownStructSlice(value reflect.Value) string {
|
||||
if value.Len() == 0 {
|
||||
return "切片/数组为空"
|
||||
}
|
||||
|
||||
firstElem := value.Index(0)
|
||||
if firstElem.Kind() == reflect.Ptr {
|
||||
firstElem = firstElem.Elem()
|
||||
}
|
||||
if firstElem.Kind() != reflect.Struct {
|
||||
return "切片/数组元素必须是结构体或结构体指针"
|
||||
}
|
||||
|
||||
t := firstElem.Type()
|
||||
var b strings.Builder
|
||||
|
||||
// 表头
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s |", getFieldName(field)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// 分隔线
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(" --- |")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// 多行数据
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
elem := value.Index(i)
|
||||
if elem.Kind() == reflect.Ptr {
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
b.WriteString("|")
|
||||
for j := 0; j < t.NumField(); j++ {
|
||||
field := t.Field(j)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
fieldValue := elem.Field(j)
|
||||
b.WriteString(fmt.Sprintf(" %s |", formatValue(fieldValue)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// 判断是否应该跳过该字段
|
||||
func shouldSkip(field reflect.StructField) bool {
|
||||
return field.Tag.Get("md") == "-"
|
||||
}
|
||||
|
||||
// 获取字段的Markdown表头名称
|
||||
func getFieldName(field reflect.StructField) string {
|
||||
name := field.Tag.Get("md")
|
||||
if name == "" || name == "-" {
|
||||
return field.Name
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// 格式化字段值为字符串
|
||||
func formatValue(value reflect.Value) string {
|
||||
if !value.IsValid() {
|
||||
return "n/a"
|
||||
}
|
||||
|
||||
// 处理指针
|
||||
if value.Kind() == reflect.Ptr {
|
||||
if value.IsNil() {
|
||||
return "nil"
|
||||
}
|
||||
return formatValue(value.Elem())
|
||||
}
|
||||
|
||||
// 处理结构体
|
||||
if value.Kind() == reflect.Struct {
|
||||
var fields []string
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
field := value.Type().Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
fieldValue := value.Field(i)
|
||||
fields = append(fields, fmt.Sprintf("%s: %s", getFieldName(field), formatValue(fieldValue)))
|
||||
}
|
||||
return "{" + strings.Join(fields, ", ") + "}"
|
||||
}
|
||||
|
||||
// 处理切片/数组
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
|
||||
var items []string
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
items = append(items, formatValue(value.Index(i)))
|
||||
}
|
||||
return "[" + strings.Join(items, ", ") + "]"
|
||||
}
|
||||
|
||||
// 处理映射
|
||||
if value.Kind() == reflect.Map {
|
||||
var items []string
|
||||
for _, key := range value.MapKeys() {
|
||||
keyStr := formatValue(key)
|
||||
valueStr := formatValue(value.MapIndex(key))
|
||||
items = append(items, fmt.Sprintf("%s: %s", keyStr, valueStr))
|
||||
}
|
||||
return "{" + strings.Join(items, ", ") + "}"
|
||||
}
|
||||
|
||||
// 基本类型
|
||||
return fmt.Sprintf("%v", value.Interface())
|
||||
}
|
||||
|
||||
// 示例结构体
|
||||
type Address struct {
|
||||
City string `md:"城市"`
|
||||
Country string `md:"国家"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `md:"姓名"`
|
||||
Age int `md:"年龄"`
|
||||
Email string `md:"邮箱"`
|
||||
Address Address `md:"地址"`
|
||||
Phones []string `md:"电话"`
|
||||
Active bool `md:"活跃状态"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 示例使用:单个结构体
|
||||
user := User{
|
||||
Name: "张三",
|
||||
Age: 30,
|
||||
Email: "zhangsan@example.com",
|
||||
Address: Address{
|
||||
City: "北京",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13800138000", "13900139000"},
|
||||
Active: true,
|
||||
}
|
||||
|
||||
fmt.Println("单个结构体转换:")
|
||||
fmt.Println(MarkdownTable(user))
|
||||
fmt.Println()
|
||||
|
||||
// 示例使用:结构体切片
|
||||
users := []User{
|
||||
{
|
||||
Name: "张三",
|
||||
Age: 30,
|
||||
Email: "zhangsan@example.com",
|
||||
Address: Address{
|
||||
City: "北京",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13800138000"},
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
Name: "李四",
|
||||
Age: 25,
|
||||
Email: "lisi@example.com",
|
||||
Address: Address{
|
||||
City: "上海",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13900139000", "13700137000"},
|
||||
Active: false,
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println("结构体切片转换:")
|
||||
fmt.Println(MarkdownTable(users))
|
||||
}
|
||||
54
backend/util/struct_to_markdown_test.go
Normal file
54
backend/util/struct_to_markdown_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMd(t *testing.T) {
|
||||
// 示例使用:单个结构体
|
||||
user := User{
|
||||
Name: "张三",
|
||||
Age: 30,
|
||||
Email: "zhangsan@example.com",
|
||||
Address: Address{
|
||||
City: "北京",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13800138000", "13900139000"},
|
||||
Active: true,
|
||||
}
|
||||
|
||||
fmt.Println("单个结构体转换:")
|
||||
fmt.Println(MarkdownTable(user))
|
||||
fmt.Println()
|
||||
|
||||
// 示例使用:结构体切片
|
||||
users := []User{
|
||||
{
|
||||
Name: "张三",
|
||||
Age: 30,
|
||||
Email: "zhangsan@example.com",
|
||||
Address: Address{
|
||||
City: "北京",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13800138000"},
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
Name: "李四",
|
||||
Age: 25,
|
||||
Email: "lisi@example.com",
|
||||
Address: Address{
|
||||
City: "上海",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13900139000", "13700137000"},
|
||||
Active: false,
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println("结构体切片转换:")
|
||||
fmt.Println(MarkdownTable(users))
|
||||
}
|
||||
@@ -180,15 +180,15 @@ const menuOptions = ref([
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "指标行情",
|
||||
name: "重大指数",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '重大指数'})
|
||||
},
|
||||
},
|
||||
{default: () => '指标行情',}
|
||||
{default: () => '重大指数',}
|
||||
),
|
||||
key: 'market3',
|
||||
icon: renderIcon(AnalyticsOutline),
|
||||
@@ -453,7 +453,7 @@ const menuOptions = ref([
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: WindowHide,
|
||||
title: '隐藏到托盘区 Ctrl+H',
|
||||
title: '隐藏到托盘区 Ctrl+Z',
|
||||
}, {default: () => '隐藏到托盘区'}),
|
||||
key: 'hide',
|
||||
icon: renderIcon(ReorderTwoOutline),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeMount, onUnmounted, ref} from 'vue'
|
||||
import {HotTopic} from "../../wailsjs/go/main/App";
|
||||
import {HotTopic, OpenURL} from "../../wailsjs/go/main/App";
|
||||
import {Environment} from "../../wailsjs/runtime";
|
||||
const list = ref([])
|
||||
const task =ref()
|
||||
|
||||
@@ -18,11 +19,20 @@ function openCenteredWindow(url, width, height) {
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
|
||||
return window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top}`
|
||||
);
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top}`
|
||||
)
|
||||
break
|
||||
default:
|
||||
OpenURL(url)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
function showPage(htid) {
|
||||
openCenteredWindow(`https://gubatopic.eastmoney.com/topic_v3.html?htid=${htid}`, 1000, 600)
|
||||
|
||||
@@ -378,7 +378,7 @@ function calculateMA(dayCount,values) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="kLineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
|
||||
<div ref="kLineChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
|
||||
import {SearchStock,GetHotStrategy} from "../../wailsjs/go/main/App";
|
||||
import {useMessage, NText, NTag,NButton} from 'naive-ui'
|
||||
import {SearchStock, GetHotStrategy, OpenURL} from "../../wailsjs/go/main/App";
|
||||
import {useMessage, NText, NTag, NButton} from 'naive-ui'
|
||||
import {Environment} from "../../wailsjs/runtime"
|
||||
import {RefreshCircleSharp} from "@vicons/ionicons5";
|
||||
|
||||
const message = useMessage()
|
||||
const search = ref('')
|
||||
const columns = ref([])
|
||||
const dataList = ref([])
|
||||
const hotStrategy = ref([])
|
||||
const traceInfo = ref('')
|
||||
|
||||
function Search() {
|
||||
if(!search.value){
|
||||
if (!search.value) {
|
||||
message.warning('请输入选股指标或者要求')
|
||||
return
|
||||
}
|
||||
@@ -18,69 +21,69 @@ function Search() {
|
||||
const loading = message.loading("正在获取选股数据...", {duration: 0});
|
||||
SearchStock(search.value).then(res => {
|
||||
loading.destroy()
|
||||
// console.log(res)
|
||||
if(res.code==100){
|
||||
traceInfo.value=res.data.traceInfo.showText
|
||||
// message.success(res.msg)
|
||||
columns.value=res.data.result.columns.filter(item=>!item.hiddenNeed&&(item.title!="市场码"&&item.title!="市场简称")).map(item=>{
|
||||
if(item.children){
|
||||
// console.log(res)
|
||||
if (res.code == 100) {
|
||||
traceInfo.value = res.data.traceInfo.showText
|
||||
// message.success(res.msg)
|
||||
columns.value = res.data.result.columns.filter(item => !item.hiddenNeed && (item.title != "市场码" && item.title != "市场简称")).map(item => {
|
||||
if (item.children) {
|
||||
return {
|
||||
title:item.title+(item.unit?'['+item.unit+']':''),
|
||||
key:item.key,
|
||||
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
|
||||
key: item.key,
|
||||
resizable: true,
|
||||
minWidth:200,
|
||||
minWidth: 200,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
children:item.children.filter(item=>!item.hiddenNeed).map(item=>{
|
||||
children: item.children.filter(item => !item.hiddenNeed).map(item => {
|
||||
return {
|
||||
title:item.dateMsg,
|
||||
key:item.key,
|
||||
minWidth:100,
|
||||
title: item.dateMsg,
|
||||
key: item.key,
|
||||
minWidth: 100,
|
||||
resizable: true,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
sorter: (row1, row2) => {
|
||||
if(isNumeric(row1[item.key])&&isNumeric(row2[item.key])){
|
||||
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
|
||||
return row1[item.key] - row2[item.key];
|
||||
}else{
|
||||
} else {
|
||||
return 'default'
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
return {
|
||||
title:item.title+(item.unit?'['+item.unit+']':''),
|
||||
key:item.key,
|
||||
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
|
||||
key: item.key,
|
||||
resizable: true,
|
||||
minWidth:120,
|
||||
minWidth: 120,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
sorter: (row1, row2) => {
|
||||
if(isNumeric(row1[item.key])&&isNumeric(row2[item.key])){
|
||||
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
|
||||
return row1[item.key] - row2[item.key];
|
||||
}else{
|
||||
} else {
|
||||
return 'default'
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
dataList.value=res.data.result.dataList
|
||||
}else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
dataList.value = res.data.result.dataList
|
||||
} else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
}).catch(err => {
|
||||
message.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
function isNumeric(value) {
|
||||
return !isNaN(parseFloat(value)) && isFinite(value);
|
||||
}
|
||||
@@ -88,9 +91,9 @@ function isNumeric(value) {
|
||||
onBeforeMount(() => {
|
||||
GetHotStrategy().then(res => {
|
||||
console.log(res)
|
||||
if(res.code==1){
|
||||
hotStrategy.value=res.data
|
||||
search.value=hotStrategy.value[0].question
|
||||
if (res.code == 1) {
|
||||
hotStrategy.value = res.data
|
||||
search.value = hotStrategy.value[0].question
|
||||
Search()
|
||||
}
|
||||
}).catch(err => {
|
||||
@@ -98,8 +101,9 @@ onBeforeMount(() => {
|
||||
})
|
||||
|
||||
})
|
||||
function DoSearch(question){
|
||||
search.value= question
|
||||
|
||||
function DoSearch(question) {
|
||||
search.value = question
|
||||
Search()
|
||||
}
|
||||
|
||||
@@ -107,60 +111,70 @@ function openCenteredWindow(url, width, height) {
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
|
||||
return window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
|
||||
);
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
|
||||
)
|
||||
break
|
||||
default:
|
||||
OpenURL(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-grid :cols="24" style="max-height: calc(100vh - 170px)">
|
||||
<n-gi :span="4" >
|
||||
<n-list bordered style="text-align: left;" hoverable clickable>
|
||||
<n-scrollbar style="max-height: calc(100vh - 170px);" >
|
||||
<n-list-item v-for="item in hotStrategy" :key="item.rank" @click="DoSearch(item.question)">
|
||||
<n-ellipsis line-clamp="1" :tooltip="true" >
|
||||
<n-tag size="small" :bordered="false" type="info">#{{item.rank}}</n-tag><n-text type="warning">{{item.question }}</n-text>
|
||||
<template #tooltip>
|
||||
<div style="text-align: center;max-width: 180px">
|
||||
<n-text type="warning">{{item.question }}</n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
</n-list-item>
|
||||
<n-gi :span="4">
|
||||
<n-list bordered style="text-align: left;" hoverable clickable>
|
||||
<n-scrollbar style="max-height: calc(100vh - 170px);">
|
||||
<n-list-item v-for="item in hotStrategy" :key="item.rank" @click="DoSearch(item.question)">
|
||||
<n-ellipsis line-clamp="1" :tooltip="true">
|
||||
<n-tag size="small" :bordered="false" type="info">#{{ item.rank }}</n-tag>
|
||||
<n-text type="warning">{{ item.question }}</n-text>
|
||||
<template #tooltip>
|
||||
<div style="text-align: center;max-width: 180px">
|
||||
<n-text type="warning">{{ item.question }}</n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
</n-list-item>
|
||||
</n-scrollbar>
|
||||
</n-list>
|
||||
|
||||
<!-- <n-virtual-list :items="hotStrategy" :item-size="hotStrategy.length">-->
|
||||
<!-- <template #default="{ item, index }">-->
|
||||
<!-- <n-card :title="''" size="small">-->
|
||||
<!-- <template #header-extra>-->
|
||||
<!-- {{item.rank}}-->
|
||||
<!-- </template>-->
|
||||
<!-- <n-ellipsis expand-trigger="click" line-clamp="3" :tooltip="false" >-->
|
||||
<!-- <n-text type="warning">{{item.question }}</n-text>-->
|
||||
<!-- </n-ellipsis>-->
|
||||
<!-- </n-card>-->
|
||||
<!-- <n-virtual-list :items="hotStrategy" :item-size="hotStrategy.length">-->
|
||||
<!-- <template #default="{ item, index }">-->
|
||||
<!-- <n-card :title="''" size="small">-->
|
||||
<!-- <template #header-extra>-->
|
||||
<!-- {{item.rank}}-->
|
||||
<!-- </template>-->
|
||||
<!-- <n-ellipsis expand-trigger="click" line-clamp="3" :tooltip="false" >-->
|
||||
<!-- <n-text type="warning">{{item.question }}</n-text>-->
|
||||
<!-- </n-ellipsis>-->
|
||||
<!-- </n-card>-->
|
||||
|
||||
<!-- </template>-->
|
||||
<!-- </n-virtual-list>-->
|
||||
<!-- </template>-->
|
||||
<!-- </n-virtual-list>-->
|
||||
</n-gi>
|
||||
<n-gi :span="20" >
|
||||
<n-gi :span="20">
|
||||
<n-flex>
|
||||
<n-input-group style="text-align: left">
|
||||
<n-input :rows="1" clearable v-model:value="search" placeholder="请输入选股指标或者要求" />
|
||||
<n-input :rows="1" clearable v-model:value="search" placeholder="请输入选股指标或者要求"/>
|
||||
<n-button type="primary" @click="Search">搜索A股</n-button>
|
||||
</n-input-group>
|
||||
</n-flex>
|
||||
<n-flex justify="start" v-if="traceInfo" style="margin: 5px 0">
|
||||
|
||||
<n-ellipsis line-clamp="1" :tooltip="true" >
|
||||
<n-text type="info" :bordered="false">选股条件:</n-text><n-text type="warning" :bordered="true">{{traceInfo}}</n-text>
|
||||
<n-ellipsis line-clamp="1" :tooltip="true">
|
||||
<n-text type="info" :bordered="false">选股条件:</n-text>
|
||||
<n-text type="warning" :bordered="true">{{ traceInfo }}</n-text>
|
||||
<template #tooltip>
|
||||
<div style="text-align: center;max-width: 580px">
|
||||
<n-text type="warning">{{traceInfo}}</n-text>
|
||||
<n-text type="warning">{{ traceInfo }}</n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
@@ -204,7 +218,10 @@ function openCenteredWindow(url, width, height) {
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<n-text>共找到<n-tag type="info" :bordered="false">{{dataList.length}}</n-tag>只股</n-text>
|
||||
<n-text>共找到
|
||||
<n-tag type="info" :bordered="false">{{ dataList.length }}</n-tag>
|
||||
只股
|
||||
</n-text>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
// preview.css相比style.css少了编辑器那部分样式
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
|
||||
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
|
||||
import {CheckUpdate, GetVersionInfo,GetSponsorInfo,OpenURL} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn,Environment} from "../../wailsjs/runtime";
|
||||
import {NAvatar, NButton, useNotification} from "naive-ui";
|
||||
const updateLog = ref('');
|
||||
const versionInfo = ref('');
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
const alipay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/alipay.jpg')
|
||||
const wxpay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/wxpay.jpg')
|
||||
const wxgzh =ref('https://github.com/ArvinLovegood/go-stock/raw/dev/build/screenshot/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png')
|
||||
const notify = useNotification()
|
||||
const vipLevel=ref("");
|
||||
const vipStartTime=ref("");
|
||||
const vipEndTime=ref("");
|
||||
|
||||
onMounted(() => {
|
||||
document.title = '关于软件';
|
||||
@@ -21,7 +25,18 @@ onMounted(() => {
|
||||
icon.value = res.icon;
|
||||
alipay.value=res.alipay;
|
||||
wxpay.value=res.wxpay;
|
||||
wxgzh.value=res.wxgzh;
|
||||
|
||||
GetSponsorInfo().then((res) => {
|
||||
vipLevel.value = res.vipLevel;
|
||||
vipStartTime.value = res.vipStartTime;
|
||||
vipEndTime.value = res.vipEndTime;
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
notify.destroyAll()
|
||||
@@ -70,7 +85,16 @@ EventsOn("updateVersion",async (msg) => {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
window.open(msg.html_url)
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(msg.html_url)
|
||||
break
|
||||
default :
|
||||
OpenURL(msg.html_url)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}, { default: () => '查看' })
|
||||
}
|
||||
@@ -87,14 +111,15 @@ EventsOn("updateVersion",async (msg) => {
|
||||
<n-space vertical >
|
||||
<n-image width="100" :src="icon" />
|
||||
<h1>
|
||||
<n-badge :value="versionInfo" :offset="[50,10]" type="success">
|
||||
<n-badge v-if="!vipLevel" :value="versionInfo" :offset="[50,10]" type="success">
|
||||
<n-gradient-text type="info" :size="50" >go-stock</n-gradient-text>
|
||||
</n-badge>
|
||||
<n-badge v-if="vipLevel" :value="versionInfo" :offset="[50,10]" type="success">
|
||||
<n-gradient-text type="warning" :size="50" >go-stock</n-gradient-text><n-tag :bordered="false" size="small" type="warning">VIP{{vipLevel}}</n-tag>
|
||||
</n-badge>
|
||||
</h1>
|
||||
<n-gradient-text type="warning" v-if="vipLevel" >vip到期时间:{{vipEndTime}}</n-gradient-text>
|
||||
<n-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
|
||||
|
||||
|
||||
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p>自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
|
||||
<p>目前已支持A股,港股,美股,未来计划加入基金,ETF等支持</p>
|
||||
@@ -113,14 +138,39 @@ EventsOn("updateVersion",async (msg) => {
|
||||
<p>QQ交流群:<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333" target="_blank">491605333</a></p>
|
||||
</div>
|
||||
</n-space>
|
||||
<n-divider title-placement="center">支持💕开源</n-divider>
|
||||
<n-flex justify="center">
|
||||
<n-table size="small" style="width: 820px">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>赞助计划</n-th>
|
||||
<n-th>赞助等级</n-th>
|
||||
<n-th>权益说明</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr>
|
||||
<n-td>每月 0 RMB</n-td><n-td>vip0</n-td><n-td>🌟 全部功能,软件自动更新(从GitHub下载),自行解决github平台网络问题。</n-td>
|
||||
</n-tr>
|
||||
<n-tr>
|
||||
<n-td>赞助 18.8 RMB/月<br>赞助 120 RMB/年</n-td><n-td>vip1</n-td><n-td>💕 全部功能,软件自动更新(从CDN下载),更新快速便捷。AI配置指导,提示词参考等</n-td>
|
||||
</n-tr>
|
||||
<n-tr>
|
||||
<n-td>赞助 28.8 RMB/月<br>赞助 240 RMB/年</n-td><n-td>vip2</n-td><n-td>💕 vip1全部功能,赠送硅基流动AI分析服务💕</n-td>
|
||||
</n-tr>
|
||||
<n-tr>
|
||||
<n-td>每月赞助 X RMB</n-td><n-td>vipX</n-td><n-td>🧩 更多计划,视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖)</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</n-flex>
|
||||
<n-divider title-placement="center">关于作者</n-divider>
|
||||
<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>
|
||||
<n-image width="300" src="https://go-stock.sparkmemory.top/assets/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88-DEJtWc_y.png" />
|
||||
|
||||
<p>一个热爱编程的小白,欢迎关注我的Github/微信公众号</p>
|
||||
<n-image width="300" :src="wxgzh" />
|
||||
<p>开源不易,如果觉得好用,可以请作者喝杯咖啡。</p>
|
||||
<n-flex justify="center">
|
||||
<n-image width="200" :src="alipay" />
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GetConfig,
|
||||
GetFollowedFund,
|
||||
GetfundList,
|
||||
GetVersionInfo,
|
||||
GetVersionInfo, OpenURL,
|
||||
UnFollowFund
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import vueDanmaku from 'vue3-danmaku'
|
||||
@@ -147,8 +147,19 @@ function formatterTitle(title){
|
||||
|
||||
function search(code,name){
|
||||
setTimeout(() => {
|
||||
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
|
||||
//window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
|
||||
//window.open("https://finance.sina.com.cn/fund/quotes/"+code+"/bc.shtml","_blank","width=1000,height=800,top=100,left=100,toolbar=no,location=no")
|
||||
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
|
||||
break
|
||||
default :
|
||||
OpenURL("https://fund.eastmoney.com/"+code+".html")
|
||||
}
|
||||
})
|
||||
|
||||
}, 500)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, ref} from 'vue'
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, ref} from 'vue'
|
||||
import {
|
||||
GetAIResponseResult,
|
||||
GetConfig,
|
||||
@@ -84,8 +84,6 @@ function getIndex() {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
onBeforeMount(() => {
|
||||
nowTab.value = route.query.name
|
||||
stockCode.value = route.query.stockCode
|
||||
@@ -311,8 +309,8 @@ function ReFlesh(source) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card style="--wails-draggable:drag">
|
||||
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
|
||||
<n-card>
|
||||
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab" style="--wails-draggable:drag">
|
||||
<n-tab-pane name="市场快讯" tab="市场快讯">
|
||||
<n-grid :cols="2" :y-gap="0">
|
||||
<n-gi>
|
||||
@@ -390,10 +388,34 @@ function ReFlesh(source) {
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="指标行情" tab="指标行情">
|
||||
<n-tab-pane name="重大指数" tab="重大指数">
|
||||
<n-tabs type="segment" animated>
|
||||
<n-tab-pane name="科创50" tab="科创50">
|
||||
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
|
||||
<n-tab-pane name="恒生科技指数" tab="恒生科技指数">
|
||||
<k-line-chart code="hkHSTECH" :chart-height="panelHeight" name="恒生科技指数" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="科创50" tab="科创50" >
|
||||
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="科创芯片" tab="科创芯片" >
|
||||
<k-line-chart code="sh000685" :chart-height="panelHeight" name="科创芯片" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="证券龙头" tab="证券龙头" >
|
||||
<k-line-chart code="sz399437" :chart-height="panelHeight" name="证券龙头" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="高端装备" tab="高端装备" >
|
||||
<k-line-chart code="sz399437" :chart-height="panelHeight" name="高端装备" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证银行" tab="中证银行">
|
||||
<k-line-chart code="sz399986" :chart-height="panelHeight" name="中证银行" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="上证医药" tab="上证医药">
|
||||
<k-line-chart code="sh000037" :chart-height="panelHeight" name="上证医药" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="沪深300" tab="沪深300">
|
||||
@@ -672,5 +694,4 @@ function ReFlesh(source) {
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GetConfig,
|
||||
GetPromptTemplates,
|
||||
SendDingDingMessageByType,
|
||||
UpdateConfig
|
||||
UpdateConfig,CheckSponsorCode
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import {NTag, useMessage} from "naive-ui";
|
||||
import {data, models} from "../../wailsjs/go/models";
|
||||
@@ -46,6 +46,7 @@ const formValue = ref({
|
||||
darkTheme:true,
|
||||
enableFund:false,
|
||||
enablePushNews:false,
|
||||
sponsorCode:"",
|
||||
})
|
||||
const promptTemplates=ref([])
|
||||
onMounted(()=>{
|
||||
@@ -80,6 +81,8 @@ onMounted(()=>{
|
||||
formValue.value.darkTheme = res.darkTheme
|
||||
formValue.value.enableFund = res.enableFund
|
||||
formValue.value.enablePushNews = res.enablePushNews
|
||||
formValue.value.sponsorCode = res.sponsorCode
|
||||
|
||||
|
||||
//console.log(res)
|
||||
})
|
||||
@@ -120,15 +123,28 @@ function saveConfig(){
|
||||
enableNews:formValue.value.enableNews,
|
||||
darkTheme:formValue.value.darkTheme,
|
||||
enableFund:formValue.value.enableFund,
|
||||
enablePushNews:formValue.value.enablePushNews
|
||||
enablePushNews:formValue.value.enablePushNews,
|
||||
sponsorCode:formValue.value.sponsorCode
|
||||
})
|
||||
|
||||
if (config.sponsorCode){
|
||||
CheckSponsorCode(config.sponsorCode).then(res=>{
|
||||
if (res.code){
|
||||
UpdateConfig(config).then(res=>{
|
||||
message.success(res)
|
||||
EventsEmit("updateSettings", config);
|
||||
})
|
||||
}else{
|
||||
message.error(res.msg)
|
||||
}
|
||||
})
|
||||
}else{
|
||||
UpdateConfig(config).then(res=>{
|
||||
message.success(res)
|
||||
EventsEmit("updateSettings", config);
|
||||
})
|
||||
}
|
||||
|
||||
//console.log("Settings",config)
|
||||
UpdateConfig(config).then(res=>{
|
||||
message.success(res)
|
||||
EventsEmit("updateSettings", config);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +215,7 @@ function importConfig(){
|
||||
formValue.value.darkTheme = config.darkTheme
|
||||
formValue.value.enableFund = config.enableFund
|
||||
formValue.value.enablePushNews = config.enablePushNews
|
||||
formValue.value.sponsorCode = config.sponsorCode
|
||||
// formRef.value.resetFields()
|
||||
};
|
||||
reader.readAsText(file);
|
||||
@@ -293,9 +310,15 @@ function deletePrompt(ID){
|
||||
<n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="指数基金:" path="enableFund" >
|
||||
<n-form-item-gi :span="3" label="指数基金:" path="enableFund" >
|
||||
<n-switch v-model:value="formValue.enableFund" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="11" label="赞助码:" path="sponsorCode" >
|
||||
<n-input-group>
|
||||
<n-input :show-count="true" placeholder="赞助码" v-model:value="formValue.sponsorCode" />
|
||||
<n-button type="success" secondary strong @click="CheckSponsorCode(formValue.sponsorCode).then((res) => {message.warning(res.msg) })">验证</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
SetStockAICron,
|
||||
SetStockSort,
|
||||
ShareAnalysis,
|
||||
UnFollow
|
||||
UnFollow,
|
||||
OpenURL,
|
||||
SaveImage,
|
||||
SaveWordFile
|
||||
} from '../../wailsjs/go/main/App'
|
||||
import {
|
||||
NAvatar,
|
||||
@@ -41,6 +44,7 @@ import {
|
||||
useNotification
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
Environment,
|
||||
EventsEmit,
|
||||
EventsOff,
|
||||
EventsOn,
|
||||
@@ -103,7 +107,7 @@ const modalShow3 = ref(false)
|
||||
const modalShow4 = ref(false)
|
||||
const modalShow5 = ref(false)
|
||||
const addBTN = ref(true)
|
||||
const enableTools= ref(false)
|
||||
const enableTools = ref(false)
|
||||
const formModel = ref({
|
||||
name: "",
|
||||
code: "",
|
||||
@@ -384,7 +388,15 @@ EventsOn("updateVersion", async (msg) => {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
window.open(msg.html_url)
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(msg.html_url)
|
||||
break
|
||||
default :
|
||||
OpenURL(msg.html_url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, {default: () => '查看'})
|
||||
}
|
||||
@@ -441,7 +453,7 @@ function AddStock() {
|
||||
Follow(data.code).then(result => {
|
||||
if (result === "关注成功") {
|
||||
if (data.code.startsWith("us")) {
|
||||
data.code= "gb_" + data.code.replace("us", "").toLowerCase()
|
||||
data.code = "gb_" + data.code.replace("us", "").toLowerCase()
|
||||
}
|
||||
stocks.value.push(data.code)
|
||||
message.success(result)
|
||||
@@ -614,12 +626,28 @@ function onSelect(item) {
|
||||
function openCenteredWindow(url, width, height) {
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
|
||||
)
|
||||
break
|
||||
default :
|
||||
OpenURL(url)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top}`
|
||||
);
|
||||
|
||||
//
|
||||
// return window.open(
|
||||
// url,
|
||||
// 'centeredWindow',
|
||||
// `width=${width},height=${height},left=${left},top=${top}`
|
||||
// );
|
||||
}
|
||||
|
||||
function search(code, name) {
|
||||
@@ -631,7 +659,7 @@ function search(code, name) {
|
||||
//window.open("https://www.iwencai.com/unifiedwap/result?w=" + name)
|
||||
//window.open("https://www.iwencai.com/chat/?question="+code)
|
||||
|
||||
openCenteredWindow("https://www.iwencai.com/unifiedwap/result?w=" + name,1000,800)
|
||||
openCenteredWindow("https://www.iwencai.com/unifiedwap/result?w=" + name, 1000, 800)
|
||||
|
||||
}, 500)
|
||||
}
|
||||
@@ -1359,7 +1387,7 @@ function aiReCheckStock(stock, stockCode) {
|
||||
//
|
||||
|
||||
//message.info("sysPromptId:"+data.sysPromptId)
|
||||
NewChatStream(stock, stockCode, data.question, data.sysPromptId,enableTools.value)
|
||||
NewChatStream(stock, stockCode, data.question, data.sysPromptId, enableTools.value)
|
||||
}
|
||||
|
||||
function aiCheckStock(stock, stockCode) {
|
||||
@@ -1437,21 +1465,42 @@ window.onerror = function (msg, source, lineno, colno, error) {
|
||||
};
|
||||
|
||||
function saveAsImage(name, code) {
|
||||
const element = document.querySelector('.md-editor-preview');
|
||||
if (element) {
|
||||
html2canvas(element, {
|
||||
useCORS: true, // 解决跨域图片问题
|
||||
scale: 2, // 提高截图质量
|
||||
allowTaint: true, // 允许跨域图片
|
||||
}).then(canvas => {
|
||||
const link = document.createElement('a');
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
link.download = name + "[" + code + ']-ai-analysis-result.png';
|
||||
link.click();
|
||||
});
|
||||
} else {
|
||||
message.error('无法找到分析结果元素');
|
||||
}
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
const element = document.querySelector('.md-editor-preview');
|
||||
if (element) {
|
||||
html2canvas(element, {
|
||||
useCORS: true, // 解决跨域图片问题
|
||||
scale: 2, // 提高截图质量
|
||||
allowTaint: true, // 允许跨域图片
|
||||
}).then(canvas => {
|
||||
const link = document.createElement('a');
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
link.download = name + "[" + code + ']-ai-analysis-result.png';
|
||||
link.click();
|
||||
});
|
||||
} else {
|
||||
message.error('无法找到分析结果元素');
|
||||
}
|
||||
break
|
||||
default :
|
||||
saveCanvasImage(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function saveCanvasImage(name) {
|
||||
const element = document.querySelector('.md-editor-preview'); // 要截图的 DOM 节点
|
||||
const canvas = await html2canvas(element)
|
||||
|
||||
const dataUrl = canvas.toDataURL('image/png') // base64 格式
|
||||
const base64 = dataUrl.replace(/^data:image\/png;base64,/, '')
|
||||
|
||||
// 调用 Go 后端保存文件(Wails 绑定方法)
|
||||
await SaveImage(name,base64).then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
}
|
||||
|
||||
async function copyToClipboard() {
|
||||
@@ -1511,13 +1560,26 @@ AI赋能股票分析:自选股行情获取,成本盈亏展示,涨跌报警
|
||||
`
|
||||
// landscape就是横着的,portrait是竖着的,默认是竖屏portrait。
|
||||
const blob = await asBlob(value, {orientation: 'portrait'})
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = `${data.name}[${data.code}]-ai-analysis-result.docx`;
|
||||
a.click()
|
||||
// 下载后将标签移除
|
||||
URL.revokeObjectURL(a.href);
|
||||
a.remove()
|
||||
const { platform } = await Environment()
|
||||
switch (platform) {
|
||||
case 'windows':
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = `${data.name}[${data.code}]-ai-analysis-result.docx`;
|
||||
a.click()
|
||||
// 下载后将标签移除
|
||||
URL.revokeObjectURL(a.href);
|
||||
a.remove()
|
||||
break
|
||||
default:
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
const binary = uint8Array.reduce((data, byte) => data + String.fromCharCode(byte), '')
|
||||
const base64 = btoa(binary)
|
||||
await SaveWordFile(`${data.name}[${data.code}]-ai-analysis-result.docx`, base64).then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function share(code, name) {
|
||||
@@ -1747,7 +1809,8 @@ function searchStockReport(stockCode) {
|
||||
取消关注
|
||||
</n-button>
|
||||
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning"
|
||||
@click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||||
AI分析
|
||||
</n-button>
|
||||
</template>
|
||||
@@ -1756,7 +1819,9 @@ function searchStockReport(stockCode) {
|
||||
<n-text :type="'info'">{{ result["日期"] + " " + result["时间"] }}</n-text>
|
||||
<n-tag size="small" v-if="result.volume>0" :type="result.profitType">{{ result.volume + "股" }}</n-tag>
|
||||
<n-tag size="small" v-if="result.costPrice>0" :type="result.profitType">
|
||||
{{ "成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )" }}
|
||||
{{
|
||||
"成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )"
|
||||
}}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
@@ -1815,7 +1880,8 @@ function searchStockReport(stockCode) {
|
||||
</n-text>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<stock-spark-line :last-price="Number(result['当前价格'])" :open-price="Number(result['昨日收盘价'])" :stock-code="result['股票代码']" :stock-name="result['股票名称']" ></stock-spark-line>
|
||||
<stock-spark-line :last-price="Number(result['当前价格'])" :open-price="Number(result['昨日收盘价'])"
|
||||
:stock-code="result['股票代码']" :stock-name="result['股票名称']"></stock-spark-line>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-grid :cols="2" :y-gap="4" :x-gap="4">
|
||||
@@ -1886,9 +1952,10 @@ function searchStockReport(stockCode) {
|
||||
取消关注
|
||||
</n-button>
|
||||
|
||||
<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>
|
||||
<n-button secondary type="error" size="tiny"
|
||||
@click="delStockGroup(result['股票代码'],result['股票名称'],group.ID)">移出分组
|
||||
</n-button>
|
||||
@@ -1898,7 +1965,9 @@ function searchStockReport(stockCode) {
|
||||
<n-text :type="'info'">{{ result["日期"] + " " + result["时间"] }}</n-text>
|
||||
<n-tag size="small" v-if="result.volume>0" :type="result.profitType">{{ result.volume + "股" }}</n-tag>
|
||||
<n-tag size="small" v-if="result.costPrice>0" :type="result.profitType">
|
||||
{{ "成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )" }}
|
||||
{{
|
||||
"成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )"
|
||||
}}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
@@ -2087,7 +2156,9 @@ function searchStockReport(stockCode) {
|
||||
不启用AI函数工具调用
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens。</n-gradient-text>
|
||||
<n-gradient-text type="error" style="margin-left: 10px">
|
||||
*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens。
|
||||
</n-gradient-text>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" style="margin-bottom: 10px">
|
||||
<n-select style="width: 49%" v-model:value="data.sysPromptId" label-field="name" value-field="ID"
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {h} from 'vue'
|
||||
import {NTag,NImage} from 'naive-ui'
|
||||
import EmbeddedUrl from "./EmbeddedUrl.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane name="百度股市通" tab="百度股市通">
|
||||
<embedded-url url="https://gushitong.baidu.com" :height="'calc(100vh - 252px)'"/>
|
||||
<n-tab-pane name="选股通" tab="选股通">
|
||||
<embedded-url url="https://xuangutong.com.cn" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<!-- <n-tab-pane name="百度股市通" tab="百度股市通">-->
|
||||
<!-- <embedded-url url="https://gushitong.baidu.com" :height="'calc(100vh - 252px)'"/>-->
|
||||
<!-- </n-tab-pane>-->
|
||||
<n-tab-pane name="东财大盘星图" tab="东财大盘星图">
|
||||
<embedded-url url="https://quote.eastmoney.com/stockhotmap/" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="TopHub" tab="TopHub(今日热榜)">
|
||||
<embedded-url url="https://tophub.today/c/finance" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="选股通" tab="选股通">
|
||||
<embedded-url url="https://xuangutong.com.cn" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="摸鱼" tab="摸鱼">
|
||||
<embedded-url url="https://996.ninja/" :height="'calc(100vh - 252px)'"/>
|
||||
</n-tab-pane>
|
||||
<!-- <n-tab-pane name="摸鱼" tab="摸鱼">-->
|
||||
<!-- <embedded-url url="https://996.ninja/" :height="'calc(100vh - 252px)'"/>-->
|
||||
<!-- </n-tab-pane>-->
|
||||
|
||||
|
||||
<n-tab-pane name="欢迎推荐更多有趣的财经网页" tab="欢迎推荐更多有趣的财经网页">
|
||||
|
||||
10
frontend/wailsjs/go/main/App.d.ts
vendored
10
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -13,6 +13,8 @@ export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
|
||||
|
||||
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
|
||||
|
||||
export function CheckSponsorCode(arg1:string):Promise<Record<string, any>>;
|
||||
|
||||
export function CheckUpdate():Promise<void>;
|
||||
|
||||
export function ClsCalendar():Promise<Array<any>>;
|
||||
@@ -49,6 +51,8 @@ export function GetMoneyRankSina(arg1:string):Promise<Array<Record<string, any>>
|
||||
|
||||
export function GetPromptTemplates(arg1:string,arg2:string):Promise<any>;
|
||||
|
||||
export function GetSponsorInfo():Promise<Record<string, any>>;
|
||||
|
||||
export function GetStockCommonKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
|
||||
|
||||
export function GetStockKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
|
||||
@@ -85,6 +89,8 @@ export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any,arg5:
|
||||
|
||||
export function NewsPush(arg1:any):Promise<void>;
|
||||
|
||||
export function OpenURL(arg1:string):Promise<void>;
|
||||
|
||||
export function ReFleshTelegraphList(arg1:string):Promise<any>;
|
||||
|
||||
export function RemoveGroup(arg1:number):Promise<string>;
|
||||
@@ -95,6 +101,10 @@ export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:st
|
||||
|
||||
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function SaveImage(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function SaveWordFile(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function SearchStock(arg1:string):Promise<Record<string, any>>;
|
||||
|
||||
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
@@ -22,6 +22,10 @@ export function AnalyzeSentiment(arg1) {
|
||||
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
|
||||
}
|
||||
|
||||
export function CheckSponsorCode(arg1) {
|
||||
return window['go']['main']['App']['CheckSponsorCode'](arg1);
|
||||
}
|
||||
|
||||
export function CheckUpdate() {
|
||||
return window['go']['main']['App']['CheckUpdate']();
|
||||
}
|
||||
@@ -94,6 +98,10 @@ export function GetPromptTemplates(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetPromptTemplates'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetSponsorInfo() {
|
||||
return window['go']['main']['App']['GetSponsorInfo']();
|
||||
}
|
||||
|
||||
export function GetStockCommonKLine(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['GetStockCommonKLine'](arg1, arg2, arg3);
|
||||
}
|
||||
@@ -166,6 +174,10 @@ export function NewsPush(arg1) {
|
||||
return window['go']['main']['App']['NewsPush'](arg1);
|
||||
}
|
||||
|
||||
export function OpenURL(arg1) {
|
||||
return window['go']['main']['App']['OpenURL'](arg1);
|
||||
}
|
||||
|
||||
export function ReFleshTelegraphList(arg1) {
|
||||
return window['go']['main']['App']['ReFleshTelegraphList'](arg1);
|
||||
}
|
||||
@@ -186,6 +198,14 @@ export function SaveAsMarkdown(arg1, arg2) {
|
||||
return window['go']['main']['App']['SaveAsMarkdown'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SaveImage(arg1, arg2) {
|
||||
return window['go']['main']['App']['SaveImage'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SaveWordFile(arg1, arg2) {
|
||||
return window['go']['main']['App']['SaveWordFile'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SearchStock(arg1) {
|
||||
return window['go']['main']['App']['SearchStock'](arg1);
|
||||
}
|
||||
|
||||
@@ -343,6 +343,7 @@ export namespace data {
|
||||
browserPoolSize: number;
|
||||
enableFund: boolean;
|
||||
enablePushNews: boolean;
|
||||
sponsorCode: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Settings(source);
|
||||
@@ -379,6 +380,7 @@ export namespace data {
|
||||
this.browserPoolSize = source["browserPoolSize"];
|
||||
this.enableFund = source["enableFund"];
|
||||
this.enablePushNews = source["enablePushNews"];
|
||||
this.sponsorCode = source["sponsorCode"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -700,6 +702,7 @@ export namespace models {
|
||||
icon: string;
|
||||
alipay: string;
|
||||
wxpay: string;
|
||||
wxgzh: string;
|
||||
buildTimeStamp: number;
|
||||
officialStatement: string;
|
||||
IsDel: number;
|
||||
@@ -719,6 +722,7 @@ export namespace models {
|
||||
this.icon = source["icon"];
|
||||
this.alipay = source["alipay"];
|
||||
this.wxpay = source["wxpay"];
|
||||
this.wxgzh = source["wxgzh"];
|
||||
this.buildTimeStamp = source["buildTimeStamp"];
|
||||
this.officialStatement = source["officialStatement"];
|
||||
this.IsDel = source["IsDel"];
|
||||
|
||||
1
go.mod
1
go.mod
@@ -13,6 +13,7 @@ require (
|
||||
github.com/go-ego/gse v0.80.3
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
|
||||
github.com/robertkrimen/otto v0.5.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.49.1
|
||||
|
||||
2
go.sum
2
go.sum
@@ -57,6 +57,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
|
||||
61
main.go
61
main.go
@@ -19,10 +19,8 @@ import (
|
||||
log "go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
@@ -40,6 +38,9 @@ var alipay []byte
|
||||
//go:embed build/screenshot/wxpay.jpg
|
||||
var wxpay []byte
|
||||
|
||||
//go:embed build/screenshot/扫码_搜索联合传播样式-白色版.png
|
||||
var wxgzh []byte
|
||||
|
||||
//go:embed build/stock_basic.json
|
||||
var stocksBin []byte
|
||||
|
||||
@@ -54,6 +55,7 @@ var stocksBinUS []byte
|
||||
var Version string
|
||||
var VersionCommit string
|
||||
var OFFICIAL_STATEMENT string
|
||||
var BuildKey string
|
||||
|
||||
func main() {
|
||||
checkDir("data")
|
||||
@@ -68,34 +70,31 @@ func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
AppMenu := menu.NewMenu()
|
||||
AppMenu.Append(menu.EditMenu())
|
||||
FileMenu := AppMenu.AddSubmenu("设置")
|
||||
FileMenu.AddText("显示搜索框", keys.CmdOrCtrl("s"), func(callbackData *menu.CallbackData) {
|
||||
runtime.EventsEmit(app.ctx, "showSearch", 1)
|
||||
})
|
||||
FileMenu.AddText("隐藏搜索框", keys.CmdOrCtrl("d"), func(callbackData *menu.CallbackData) {
|
||||
runtime.EventsEmit(app.ctx, "showSearch", 0)
|
||||
})
|
||||
FileMenu.AddText("刷新数据", keys.CmdOrCtrl("r"), func(callbackData *menu.CallbackData) {
|
||||
//runtime.EventsEmit(app.ctx, "refresh", "setting-"+time.Now().Format("2006-01-02 15:04:05"))
|
||||
runtime.EventsEmit(app.ctx, "refreshFollowList", "refresh-"+time.Now().Format("2006-01-02 15:04:05"))
|
||||
})
|
||||
FileMenu.AddSeparator()
|
||||
FileMenu.AddText("窗口全屏", keys.CmdOrCtrl("f"), func(callback *menu.CallbackData) {
|
||||
runtime.WindowFullscreen(app.ctx)
|
||||
})
|
||||
FileMenu.AddText("窗口还原", keys.Key("Esc"), func(callback *menu.CallbackData) {
|
||||
runtime.WindowUnfullscreen(app.ctx)
|
||||
})
|
||||
//FileMenu.AddText("显示搜索框", keys.CmdOrCtrl("s"), func(callbackData *menu.CallbackData) {
|
||||
// runtime.EventsEmit(app.ctx, "showSearch", 1)
|
||||
//})
|
||||
//FileMenu.AddText("隐藏搜索框", keys.CmdOrCtrl("d"), func(callbackData *menu.CallbackData) {
|
||||
// runtime.EventsEmit(app.ctx, "showSearch", 0)
|
||||
//})
|
||||
//FileMenu.AddText("刷新数据", keys.CmdOrCtrl("r"), func(callbackData *menu.CallbackData) {
|
||||
// //runtime.EventsEmit(app.ctx, "refresh", "setting-"+time.Now().Format("2006-01-02 15:04:05"))
|
||||
// runtime.EventsEmit(app.ctx, "refreshFollowList", "refresh-"+time.Now().Format("2006-01-02 15:04:05"))
|
||||
//})
|
||||
//FileMenu.AddSeparator()
|
||||
|
||||
if goruntime.GOOS == "windows" {
|
||||
FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("h"), func(_ *menu.CallbackData) {
|
||||
runtime.WindowHide(app.ctx)
|
||||
})
|
||||
|
||||
FileMenu.AddText("显示", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {
|
||||
runtime.WindowShow(app.ctx)
|
||||
})
|
||||
}
|
||||
//if goruntime.GOOS == "windows" {
|
||||
// FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("z"), func(_ *menu.CallbackData) {
|
||||
// runtime.WindowHide(app.ctx)
|
||||
// })
|
||||
//}
|
||||
|
||||
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
|
||||
// runtime.Quit(app.ctx)
|
||||
@@ -106,7 +105,7 @@ func main() {
|
||||
//var width, height int
|
||||
//var err error
|
||||
//
|
||||
width, _, err := getScreenResolution()
|
||||
width, _, minWidth, minHeight, err := getScreenResolution()
|
||||
if err != nil {
|
||||
log.SugaredLogger.Error("get screen resolution error")
|
||||
width = 1456
|
||||
@@ -119,18 +118,20 @@ func main() {
|
||||
backgroundColour = &options.RGBA{R: 27, G: 38, B: 54, A: 1}
|
||||
}
|
||||
|
||||
frameless := getFrameless()
|
||||
|
||||
// Create application with options
|
||||
err = wails.Run(&options.App{
|
||||
Title: "go-stock",
|
||||
Width: width * 4 / 5,
|
||||
Height: 900,
|
||||
MinWidth: 1456,
|
||||
MinHeight: 768,
|
||||
MinWidth: minWidth,
|
||||
MinHeight: minHeight,
|
||||
//MaxWidth: width,
|
||||
//MaxHeight: height,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: true,
|
||||
Frameless: frameless,
|
||||
StartHidden: false,
|
||||
HideWindowOnClose: false,
|
||||
EnableDefaultContextMenu: true,
|
||||
@@ -163,12 +164,11 @@ func main() {
|
||||
// Mac platform specific options
|
||||
Mac: &mac.Options{
|
||||
TitleBar: &mac.TitleBar{
|
||||
TitlebarAppearsTransparent: true,
|
||||
TitlebarAppearsTransparent: false,
|
||||
HideTitle: false,
|
||||
HideTitleBar: false,
|
||||
FullSizeContent: false,
|
||||
UseToolbar: false,
|
||||
HideToolbarSeparator: true,
|
||||
UseToolbar: true,
|
||||
},
|
||||
Appearance: mac.NSAppearanceNameDarkAqua,
|
||||
WebviewIsTransparent: true,
|
||||
@@ -346,6 +346,9 @@ func checkDir(dir string) {
|
||||
os.Mkdir(dir, os.ModePerm)
|
||||
log.SugaredLogger.Info("create dir: " + dir)
|
||||
}
|
||||
if BuildKey == "" {
|
||||
BuildKey = "cc1e0d684e32f176c56ff1fcf384dcd9"
|
||||
}
|
||||
}
|
||||
|
||||
// PanicHandler 捕获 panic 的包装函数
|
||||
|
||||
23
utils.go
Normal file
23
utils.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/7/8 18:51
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
import "runtime"
|
||||
|
||||
// IsWindows 判断是否为 Windows 系统
|
||||
func IsWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
|
||||
// IsMacOS 判断是否为 macOS 系统
|
||||
func IsMacOS() bool {
|
||||
return runtime.GOOS == "darwin"
|
||||
}
|
||||
|
||||
// IsLinux 判断是否为 Linux 系统
|
||||
func IsLinux() bool {
|
||||
return runtime.GOOS == "linux"
|
||||
}
|
||||
Reference in New Issue
Block a user