Compare commits
145 Commits
1.0.0
...
v2025.2.8.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86183f4585 | ||
|
|
97ab29259a | ||
|
|
91f3e66239 | ||
|
|
713b25d2db | ||
|
|
d0b65e7063 | ||
|
|
f062306158 | ||
|
|
ae7b617e83 | ||
|
|
1035f2a800 | ||
|
|
cb28b18541 | ||
|
|
9d42eb2729 | ||
|
|
7b93d4d8ca | ||
|
|
3e13ef007b | ||
|
|
6ff1b68f1b | ||
|
|
a6547db195 | ||
|
|
567414a136 | ||
|
|
34dc38a95f | ||
|
|
6d2ab3ef41 | ||
|
|
e55506705e | ||
|
|
322e87efbd | ||
|
|
1628381295 | ||
|
|
8afc26badb | ||
|
|
d5db2ef879 | ||
|
|
509cd2dbca | ||
|
|
3de2ad3cdc | ||
|
|
b00bddcdec | ||
|
|
64b37b687c | ||
|
|
e81319bb4f | ||
|
|
7bc219d1a5 | ||
|
|
0f2f58e6b8 | ||
|
|
2dc0b95b45 | ||
|
|
869eced99e | ||
|
|
f5aa70bf61 | ||
|
|
71289d1408 | ||
|
|
f6297d224c | ||
|
|
0bfa50e2b6 | ||
|
|
0d182b923f | ||
|
|
7f19d00a23 | ||
|
|
f514a0083d | ||
|
|
7fbe178c78 | ||
|
|
40fe30ce2f | ||
|
|
1751be729b | ||
|
|
d82ace220a | ||
|
|
9c51ecde2f | ||
|
|
d3cf202c88 | ||
|
|
847cacc71e | ||
|
|
23149b8a28 | ||
|
|
698f496a3c | ||
|
|
8ac97f43ff | ||
|
|
0abf7c9e5a | ||
|
|
a55920f445 | ||
|
|
775635a48c | ||
|
|
5bc7cfab0a | ||
|
|
e3e06d342b | ||
|
|
16e187b96c | ||
|
|
399513cf14 | ||
|
|
3f024faf82 | ||
|
|
dadfe1cf54 | ||
|
|
9cd6761778 | ||
|
|
1d58d6b224 | ||
|
|
db4a2b5fa9 | ||
|
|
af3f2b03dc | ||
|
|
ccbb835c83 | ||
|
|
97b5faee4a | ||
|
|
c9a8192d60 | ||
|
|
1af138312a | ||
|
|
272c990248 | ||
|
|
2907285915 | ||
|
|
9f7b7b8a64 | ||
|
|
02bfe4758e | ||
|
|
6483243d2a | ||
|
|
1ea534b3c0 | ||
|
|
2fcd89ab97 | ||
|
|
b5c44870fe | ||
|
|
54f0a0b585 | ||
|
|
ab6f400930 | ||
|
|
a376d1d92c | ||
|
|
a653ef9fa8 | ||
|
|
ce29514b54 | ||
|
|
1fd149bbd5 | ||
|
|
9dc8fa97df | ||
|
|
7c52cd1d26 | ||
|
|
338ce91ffd | ||
|
|
6e5f57d62e | ||
|
|
b1a0e9575b | ||
|
|
88fb3ce94c | ||
|
|
60d8efc158 | ||
|
|
a41ab5499a | ||
|
|
a54f769ea2 | ||
|
|
9a46788339 | ||
|
|
def92ad722 | ||
|
|
7e27996f17 | ||
|
|
ad428f83f8 | ||
|
|
d3c6c1d570 | ||
|
|
1554d3309d | ||
|
|
e7560f3e9b | ||
|
|
daa29b37a5 | ||
|
|
9a41560bee | ||
|
|
975ad611df | ||
|
|
b764770729 | ||
|
|
88aa793774 | ||
|
|
180bec8866 | ||
|
|
420d5b60f1 | ||
|
|
af1bc685a7 | ||
|
|
b0922b0878 | ||
|
|
200a160acf | ||
|
|
9fae9fc034 | ||
|
|
9a3393bfc3 | ||
|
|
64270d5df2 | ||
|
|
e808ca47b6 | ||
|
|
1b3c043ce6 | ||
|
|
04446d7521 | ||
|
|
2306a8e225 | ||
|
|
46aff404d4 | ||
|
|
1e5d9bc469 | ||
|
|
98844ce717 | ||
|
|
2bd91c6555 | ||
|
|
2166b0a39b | ||
|
|
2f2b19f5d7 | ||
|
|
685a7d23b2 | ||
|
|
5f1eaf02c4 | ||
|
|
116dae19cf | ||
|
|
a35b42f831 | ||
|
|
513cd69e3e | ||
|
|
0ce01bcdf0 | ||
|
|
afe5474264 | ||
|
|
f35847823b | ||
|
|
15120c98da | ||
|
|
6903abd6f8 | ||
|
|
f9a0c8d94e | ||
|
|
0c04272153 | ||
|
|
cf7e8415e6 | ||
|
|
1ab6875790 | ||
|
|
cc40da8371 | ||
|
|
29158f51af | ||
|
|
9665462ae5 | ||
|
|
71e3953cd8 | ||
|
|
bedeaad1f2 | ||
|
|
4f05820e2e | ||
|
|
abf05aabd8 | ||
|
|
9016289e96 | ||
|
|
bab53711b7 | ||
|
|
de67f07f41 | ||
|
|
869223cfb9 | ||
|
|
d240239fcc | ||
|
|
6b62385cad |
15
.github/workflows/main.yml
vendored
@@ -17,9 +17,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: 'go-stock'
|
||||
- name: 'go-stock-windows-amd64.exe'
|
||||
platform: 'windows/amd64'
|
||||
os: 'windows-latest'
|
||||
# - name: 'go-stock-linux-amd64'
|
||||
# platform: 'linux/amd64'
|
||||
# os: 'ubuntu-latest'
|
||||
|
||||
runs-on: ${{ matrix.build.os }}
|
||||
steps:
|
||||
@@ -28,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: dAppServer/wails-build-action@v2.2
|
||||
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 }}
|
||||
|
||||
7
.gitignore
vendored
@@ -106,6 +106,7 @@ dist
|
||||
.DS_Store
|
||||
|
||||
.idea/
|
||||
data/*.db
|
||||
build/*.exe
|
||||
/build/bin/go-stock-dev.exe
|
||||
/data/*.db
|
||||
/build/*.exe
|
||||
/build/bin/*
|
||||
frontend/package.json.md5
|
||||
37
README.md
@@ -1,11 +1,35 @@
|
||||
# README
|
||||
|
||||

|
||||
## 自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具
|
||||
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- 欢迎大家提出宝贵的建议,欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧),谢谢。
|
||||
## BIG NEWS !!! 重大更新!!!
|
||||
- 2025.01.17 新增AI大模型分析股票功能
|
||||

|
||||
|
||||
## 简介
|
||||
- 本项目基于Wails和NaiveUI,纯属无聊,仅供娱乐,不喜勿喷。
|
||||
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
|
||||
|
||||
|
||||
## Snapshot
|
||||

|
||||
### 设置
|
||||

|
||||
### 成本设置
|
||||

|
||||
### 日K
|
||||

|
||||
### 分时
|
||||

|
||||
### 钉钉报警通知
|
||||

|
||||
### AI分析股票
|
||||

|
||||
### 版本信息提示
|
||||

|
||||
## About
|
||||
|
||||
A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
|
||||
### A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
|
||||
A股数据可视化工具,基于Wails和NaiveUI。
|
||||
|
||||
## Prerequisites
|
||||
@@ -28,4 +52,9 @@ You can build you Application with: `wails build`
|
||||
[NaiveUI](https://www.naiveui.com/)
|
||||
[Wails](https://wails.io/)
|
||||
[Vue](https://vuejs.org/)
|
||||
[Vite](https://vitejs.dev/)
|
||||
[Vite](https://vitejs.dev/)
|
||||
|
||||
## 都划到这了,如果我的项目对您有帮助,请赞助我吧!😊😊😊
|
||||
| 支付宝 | 微信 |
|
||||
|-----|-----|
|
||||
|  |  |
|
||||
483
app.go
@@ -1,46 +1,357 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"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/getlantern/systray"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &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
|
||||
|
||||
// 创建系统托盘
|
||||
systray.Run(func() {
|
||||
go onReady(a)
|
||||
}, func() {
|
||||
go onExit(a)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
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(name string) *data.StockInfo {
|
||||
stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(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
|
||||
}
|
||||
|
||||
@@ -63,3 +374,165 @@ func (a *App) GetStockList(key string) []data.StockBasic {
|
||||
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 (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 {
|
||||
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 onExit(a *App) {
|
||||
// 清理操作
|
||||
logger.SugaredLogger.Infof("onExit")
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
func onReady(a *App) {
|
||||
|
||||
// 初始化操作
|
||||
logger.SugaredLogger.Infof("onReady")
|
||||
systray.SetIcon(icon2)
|
||||
systray.SetTitle("go-stock")
|
||||
systray.SetTooltip("go-stock 股票行情实时获取")
|
||||
// 创建菜单项
|
||||
show := systray.AddMenuItem("显示", "显示应用程序")
|
||||
hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
|
||||
systray.AddSeparator()
|
||||
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
|
||||
|
||||
// 监听菜单项点击事件
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mQuitOrig.ClickedCh:
|
||||
logger.SugaredLogger.Infof("退出应用程序")
|
||||
runtime.Quit(a.ctx)
|
||||
//systray.Quit()
|
||||
case <-show.ClickedCh:
|
||||
logger.SugaredLogger.Infof("显示应用程序")
|
||||
runtime.WindowShow(a.ctx)
|
||||
//runtime.WindowShow(a.ctx)
|
||||
case <-hide.ClickedCh:
|
||||
logger.SugaredLogger.Infof("隐藏应用程序")
|
||||
runtime.WindowHide(a.ctx)
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
459
app_darwin.go
Normal 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()
|
||||
}
|
||||
193
app_linux.go
Normal file
@@ -0,0 +1,193 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/logger"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
}
|
||||
|
||||
// domReady is called after front-end resources have been loaded
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
// Add your action here
|
||||
//ticker := time.NewTicker(time.Second)
|
||||
//defer ticker.Stop()
|
||||
////定时更新数据
|
||||
//go func() {
|
||||
// for range ticker.C {
|
||||
// runtime.WindowSetTitle(ctx, "go-stock "+time.Now().Format("2006-01-02 15:04:05"))
|
||||
// }
|
||||
//}()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(name string) *data.StockInfo {
|
||||
stockDatas, _ := data.NewStockDataApi().GetStockCodeRealTimeData(name)
|
||||
stockData := (*stockDatas)[0]
|
||||
return &stockData
|
||||
}
|
||||
|
||||
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) 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)
|
||||
}
|
||||
|
||||
func (a *App) SetStockSort(sort int64, stockCode string) {
|
||||
data.NewStockDataApi().SetStockSort(sort, stockCode)
|
||||
}
|
||||
|
||||
// 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 ""
|
||||
}
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
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 {
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
}
|
||||
52
backend/data/alert_darwin_api.go
Normal 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
|
||||
}
|
||||
32
backend/data/alert_darwin_api_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
53
backend/data/alert_windows_api.go
Normal file
@@ -0,0 +1,53 @@
|
||||
//go:build windows
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-toast/toast"
|
||||
"go-stock/backend/logger"
|
||||
)
|
||||
|
||||
// AlertWindowsApi @Author spark
|
||||
// @Date 2025/1/8 9:40
|
||||
// @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
|
||||
}
|
||||
|
||||
notification := toast.Notification{
|
||||
AppID: a.AppID,
|
||||
Title: a.Title,
|
||||
Message: a.Content,
|
||||
Icon: a.Icon,
|
||||
Duration: "short",
|
||||
Audio: toast.Default,
|
||||
}
|
||||
err := notification.Push()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
30
backend/data/alert_windows_api_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build windows
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-toast/toast"
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/8 9:40
|
||||
// @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
|
||||
}
|
||||
}
|
||||
85
backend/data/dingding_api.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/logger"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/3 13:53
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
type DingDingAPI struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewDingDingAPI() *DingDingAPI {
|
||||
return &DingDingAPI{
|
||||
client: resty.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
if getConfig().DingPushEnable == false {
|
||||
logger.SugaredLogger.Info("钉钉推送未开启")
|
||||
return "钉钉推送未开启"
|
||||
}
|
||||
// 发送钉钉消息
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(message).
|
||||
Post(getApiURL())
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "发送钉钉消息失败"
|
||||
}
|
||||
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
|
||||
return "发送钉钉消息成功"
|
||||
}
|
||||
func getConfig() *Settings {
|
||||
return NewSettingsApi(&Settings{}).GetConfig()
|
||||
}
|
||||
func getApiURL() string {
|
||||
return getConfig().DingRobot
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendToDingDing(title, message string) string {
|
||||
// 发送钉钉消息
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(&Message{
|
||||
Msgtype: "markdown",
|
||||
Markdown: Markdown{
|
||||
Title: "go-stock " + title,
|
||||
Text: message,
|
||||
},
|
||||
At: At{
|
||||
IsAtAll: true,
|
||||
},
|
||||
}).
|
||||
Post(getApiURL())
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "发送钉钉消息失败"
|
||||
}
|
||||
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
|
||||
return "发送钉钉消息成功"
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Msgtype string `json:"msgtype"`
|
||||
Markdown Markdown `json:"markdown"`
|
||||
At At `json:"at"`
|
||||
}
|
||||
|
||||
type Markdown struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type At struct {
|
||||
AtMobiles []string `json:"atMobiles"`
|
||||
AtUserIds []string `json:"atUserIds"`
|
||||
IsAtAll bool `json:"isAtAll"`
|
||||
}
|
||||
32
backend/data/dingding_api_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/3 13:53
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestRobot(t *testing.T) {
|
||||
dingdingRobotUrl := "XXX"
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(`{
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title":"go-stock",
|
||||
"text": "#### 杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%\n > \n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n"
|
||||
},
|
||||
"at": {
|
||||
"isAtAll": true
|
||||
}
|
||||
}`).
|
||||
Post(dingdingRobotUrl)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(resp.String())
|
||||
}
|
||||
467
backend/data/openai_api.go
Normal file
@@ -0,0 +1,467 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"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"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/16 13:19
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type OpenAi struct {
|
||||
BaseUrl string `json:"base_url"`
|
||||
ApiKey string `json:"api_key"`
|
||||
Model string `json:"model"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Prompt string `json:"prompt"`
|
||||
TimeOut int `json:"time_out"`
|
||||
}
|
||||
|
||||
func NewDeepSeekOpenAi() *OpenAi {
|
||||
config := getConfig()
|
||||
return &OpenAi{
|
||||
BaseUrl: config.OpenAiBaseUrl,
|
||||
ApiKey: config.OpenAiApiKey,
|
||||
Model: config.OpenAiModelName,
|
||||
MaxTokens: config.OpenAiMaxTokens,
|
||||
Temperature: config.OpenAiTemperature,
|
||||
Prompt: config.Prompt,
|
||||
TimeOut: config.OpenAiApiTimeOut,
|
||||
}
|
||||
}
|
||||
|
||||
type THSTokenResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type AiResponse struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Index int `json:"index"`
|
||||
Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
Logprobs interface{} `json:"logprobs"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
PromptCacheHitTokens int `json:"prompt_cache_hit_tokens"`
|
||||
PromptCacheMissTokens int `json:"prompt_cache_miss_tokens"`
|
||||
} `json:"usage"`
|
||||
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() {
|
||||
defer close(ch)
|
||||
msg := []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
|
||||
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师,精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会,在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合,注重风险控制。\n\n【核心功能】\n\n市场分析维度:\n\n宏观经济(GDP/CPI/货币政策)\n\n行业景气度(产业链/政策红利/技术革新)\n\n个股三维诊断:\n\n基本面:PE/PB/ROE/现金流/护城河\n\n技术面:K线形态/均线系统/量价关系/指标背离\n\n资金面:主力动向/北向资金/融资余额/大宗交易\n\n智能策略库:\n√ 趋势跟踪策略(鳄鱼线+ADX)\n√ 波段交易策略(斐波那契回撤+RSI)\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系:\n▶ 动态止损:ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令(行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析:\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议(附压力测试结果)\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑(3点关键驱动力)\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时,采用苏格拉底式追问:\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%?\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式:\n📈 标的名称:XXXXXX\n⚖️ 多空信号:金叉确认/顶背离预警\n🎯 关键价位:支撑位XX.XX/压力位XX.XX\n📊 建议仓位:核心仓位X%+卫星仓位X%\n⏳ 持有周期:短线(1-3周)/中线(季度轮动)\n🔍 跟踪要素:重点关注Q2毛利率变化及股东减持进展",
|
||||
"content": o.Prompt,
|
||||
},
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(5)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockPriceInfo(stockCode)
|
||||
price := ""
|
||||
for _, message := range *messages {
|
||||
price += message + ";"
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": stock + "当前价格:" + price,
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := GetFinancialReports(stockCode)
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": stock + message,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := GetTelegraphList()
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": message,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockInfo(stock, "depth")
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": message,
|
||||
})
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockInfo(stock, "telegram")
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": message,
|
||||
})
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "分析和总结",
|
||||
})
|
||||
client := resty.New()
|
||||
client.SetBaseURL(o.BaseUrl)
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
client.SetRetryCount(3)
|
||||
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{}{
|
||||
"model": o.Model,
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
"messages": msg,
|
||||
}).
|
||||
Post("/chat/completions")
|
||||
|
||||
defer resp.RawBody().Close()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
|
||||
ch <- 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 != "" {
|
||||
ch <- content
|
||||
logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
}
|
||||
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
|
||||
ch <- reasoningContent
|
||||
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
|
||||
}
|
||||
if choice.FinishReason == "stop" {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
|
||||
ch <- err.Error()
|
||||
}
|
||||
} else {
|
||||
ch <- line
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func GetFinancialReports(stockCode string) *[]string {
|
||||
// 创建一个 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/snowman/S/%s/detail#/ZYCWZB", stockCode)
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(url),
|
||||
// 等待页面加载完成,可以根据需要调整等待时间
|
||||
chromedp.WaitVisible("table.table", 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 &[]string{}
|
||||
}
|
||||
var messages []string
|
||||
document.Find("table tr").Each(func(i int, selection *goquery.Selection) {
|
||||
tr := ""
|
||||
selection.Find("th,td").Each(func(i int, selection *goquery.Selection) {
|
||||
ret := selection.Find("p").First().Text()
|
||||
if ret == "" {
|
||||
ret = selection.Text()
|
||||
}
|
||||
text := strutil.RemoveNonPrintable(ret)
|
||||
tr += text + " "
|
||||
})
|
||||
logger.SugaredLogger.Infof("%s", tr+" \n")
|
||||
messages = append(messages, tr+" \n")
|
||||
})
|
||||
return &messages
|
||||
}
|
||||
|
||||
func (o OpenAi) NewCommonChatStream(stock, stockCode, apiURL, apiKey, Model string) <-chan string {
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
client := resty.New()
|
||||
client.SetHeader("Authorization", "Bearer "+apiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
client.SetRetryCount(3)
|
||||
|
||||
msg := []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
"content": o.Prompt,
|
||||
},
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockPriceInfo(stockCode)
|
||||
price := ""
|
||||
for _, message := range *messages {
|
||||
price += message + ";"
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": stock + "当前价格:" + price,
|
||||
})
|
||||
}()
|
||||
//go func() {
|
||||
// defer wg.Done()
|
||||
// messages := SearchStockInfo(stock, "depth")
|
||||
// for _, message := range *messages {
|
||||
// msg = append(msg, map[string]interface{}{
|
||||
// "role": "assistant",
|
||||
// "content": message,
|
||||
// })
|
||||
// }
|
||||
//}()
|
||||
//go func() {
|
||||
// defer wg.Done()
|
||||
// messages := SearchStockInfo(stock, "telegram")
|
||||
// for _, message := range *messages {
|
||||
// msg = append(msg, map[string]interface{}{
|
||||
// "role": "assistant",
|
||||
// "content": message,
|
||||
// })
|
||||
// }
|
||||
//}()
|
||||
wg.Wait()
|
||||
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "分析和总结",
|
||||
})
|
||||
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": Model,
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
"messages": msg,
|
||||
}).
|
||||
Post(apiURL)
|
||||
|
||||
if err != nil {
|
||||
ch <- err.Error()
|
||||
return
|
||||
}
|
||||
defer resp.RawBody().Close()
|
||||
|
||||
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"`
|
||||
} `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 != "" {
|
||||
ch <- content
|
||||
}
|
||||
if choice.FinishReason == "stop" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func GetTelegraphList() *[]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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
21
backend/data/openai_api_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
ai := NewDeepSeekOpenAi()
|
||||
res := ai.NewChatStream("北京文化", "sz000802")
|
||||
for {
|
||||
select {
|
||||
case msg := <-res:
|
||||
if msg == "" {
|
||||
continue
|
||||
}
|
||||
t.Log(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
90
backend/data/settings_api.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
gorm.Model
|
||||
TushareToken string `json:"tushareToken"`
|
||||
LocalPushEnable bool `json:"localPushEnable"`
|
||||
DingPushEnable bool `json:"dingPushEnable"`
|
||||
DingRobot string `json:"dingRobot"`
|
||||
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
|
||||
RefreshInterval int64 `json:"refreshInterval"`
|
||||
|
||||
OpenAiEnable bool `json:"openAiEnable"`
|
||||
OpenAiBaseUrl string `json:"openAiBaseUrl"`
|
||||
OpenAiApiKey string `json:"openAiApiKey"`
|
||||
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 {
|
||||
return "settings"
|
||||
}
|
||||
|
||||
type SettingsApi struct {
|
||||
Config Settings
|
||||
}
|
||||
|
||||
func NewSettingsApi(settings *Settings) *SettingsApi {
|
||||
return &SettingsApi{
|
||||
Config: *settings,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SettingsApi) UpdateConfig() string {
|
||||
count := int64(0)
|
||||
db.Dao.Model(s.Config).Count(&count)
|
||||
if count > 0 {
|
||||
db.Dao.Model(s.Config).Where("id=?", s.Config.ID).Updates(map[string]any{
|
||||
"local_push_enable": s.Config.LocalPushEnable,
|
||||
"ding_push_enable": s.Config.DingPushEnable,
|
||||
"ding_robot": s.Config.DingRobot,
|
||||
"update_basic_info_on_start": s.Config.UpdateBasicInfoOnStart,
|
||||
"refresh_interval": s.Config.RefreshInterval,
|
||||
"open_ai_enable": s.Config.OpenAiEnable,
|
||||
"open_ai_base_url": s.Config.OpenAiBaseUrl,
|
||||
"open_ai_api_key": s.Config.OpenAiApiKey,
|
||||
"open_ai_model_name": s.Config.OpenAiModelName,
|
||||
"open_ai_max_tokens": s.Config.OpenAiMaxTokens,
|
||||
"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)
|
||||
db.Dao.Model(s.Config).Create(&Settings{
|
||||
LocalPushEnable: s.Config.LocalPushEnable,
|
||||
DingPushEnable: s.Config.DingPushEnable,
|
||||
DingRobot: s.Config.DingRobot,
|
||||
UpdateBasicInfoOnStart: s.Config.UpdateBasicInfoOnStart,
|
||||
RefreshInterval: s.Config.RefreshInterval,
|
||||
OpenAiEnable: s.Config.OpenAiEnable,
|
||||
OpenAiBaseUrl: s.Config.OpenAiBaseUrl,
|
||||
OpenAiApiKey: s.Config.OpenAiApiKey,
|
||||
OpenAiModelName: s.Config.OpenAiModelName,
|
||||
OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
|
||||
OpenAiTemperature: s.Config.OpenAiTemperature,
|
||||
TushareToken: s.Config.TushareToken,
|
||||
Prompt: s.Config.Prompt,
|
||||
CheckUpdate: s.Config.CheckUpdate,
|
||||
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
|
||||
})
|
||||
}
|
||||
return "保存成功!"
|
||||
}
|
||||
func (s SettingsApi) GetConfig() *Settings {
|
||||
var settings Settings
|
||||
db.Dao.Model(&Settings{}).First(&settings)
|
||||
return &settings
|
||||
}
|
||||
@@ -6,64 +6,85 @@ package data
|
||||
//-----------------------------------------------------------------------------------
|
||||
import (
|
||||
"bytes"
|
||||
"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/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/duke-git/lancet/v2/validator"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
"gorm.io/gorm"
|
||||
"io/ioutil"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// http://hq.sinajs.cn/rn=1730966120830&list=sh600000,sh600859
|
||||
const sina_stook_url = "http://hq.sinajs.cn/rn=%d&list=%s"
|
||||
const tushare_api_url = "http://api.tushare.pro"
|
||||
const TushareToken = "9125ec636217a99a3218a64fc63507e95205f2666590792923cbaedf"
|
||||
const sinaStockUrl = "http://hq.sinajs.cn/rn=%d&list=%s"
|
||||
const tushareApiUrl = "http://api.tushare.pro"
|
||||
|
||||
type StockDataApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
}
|
||||
type StockInfo struct {
|
||||
gorm.Model
|
||||
Date string `json:"日期" gorm:"index"`
|
||||
Time string `json:"时间" gorm:"index"`
|
||||
Code string `json:"股票代码" gorm:"index"`
|
||||
Name string `json:"股票名称" gorm:"index"`
|
||||
Price string `json:"当前价格"`
|
||||
Volume string `json:"成交的股票数"`
|
||||
Amount string `json:"成交金额"`
|
||||
Open string `json:"今日开盘价"`
|
||||
PreClose string `json:"昨日收盘价"`
|
||||
High string `json:"今日最低价"`
|
||||
Low string `json:"今日最高价"`
|
||||
Bid string `json:"竞买价"`
|
||||
Ask string `json:"竞卖价"`
|
||||
B1P string `json:"买一报价"`
|
||||
B1V string `json:"买一申报"`
|
||||
B2P string `json:"买二报价"`
|
||||
B2V string `json:"买二申报"`
|
||||
B3P string `json:"买三报价"`
|
||||
B3V string `json:"买三申报"`
|
||||
B4P string `json:"买四报价"`
|
||||
B4V string `json:"买四申报"`
|
||||
B5P string `json:"买五报价"`
|
||||
B5V string `json:"买五申报"`
|
||||
A1P string `json:"卖一报价"`
|
||||
A1V string `json:"卖一申报"`
|
||||
A2P string `json:"卖二报价"`
|
||||
A2V string `json:"卖二申报"`
|
||||
A3P string `json:"卖三报价"`
|
||||
A3V string `json:"卖三申报"`
|
||||
A4P string `json:"卖四报价"`
|
||||
A4V string `json:"卖四申报"`
|
||||
A5P string `json:"卖五报价"`
|
||||
A5V string `json:"卖五申报"`
|
||||
Date string `json:"日期" gorm:"index"`
|
||||
Time string `json:"时间" gorm:"index"`
|
||||
Code string `json:"股票代码" gorm:"index"`
|
||||
Name string `json:"股票名称" gorm:"index"`
|
||||
PrePrice float64 `json:"上次当前价格"`
|
||||
Price string `json:"当前价格"`
|
||||
Volume string `json:"成交的股票数"`
|
||||
Amount string `json:"成交金额"`
|
||||
Open string `json:"今日开盘价"`
|
||||
PreClose string `json:"昨日收盘价"`
|
||||
High string `json:"今日最高价"`
|
||||
Low string `json:"今日最低价"`
|
||||
Bid string `json:"竞买价"`
|
||||
Ask string `json:"竞卖价"`
|
||||
B1P string `json:"买一报价"`
|
||||
B1V string `json:"买一申报"`
|
||||
B2P string `json:"买二报价"`
|
||||
B2V string `json:"买二申报"`
|
||||
B3P string `json:"买三报价"`
|
||||
B3V string `json:"买三申报"`
|
||||
B4P string `json:"买四报价"`
|
||||
B4V string `json:"买四申报"`
|
||||
B5P string `json:"买五报价"`
|
||||
B5V string `json:"买五申报"`
|
||||
A1P string `json:"卖一报价"`
|
||||
A1V string `json:"卖一申报"`
|
||||
A2P string `json:"卖二报价"`
|
||||
A2V string `json:"卖二申报"`
|
||||
A3P string `json:"卖三报价"`
|
||||
A3V string `json:"卖三申报"`
|
||||
A4P string `json:"卖四报价"`
|
||||
A4V string `json:"卖四申报"`
|
||||
A5P string `json:"卖五报价"`
|
||||
A5V string `json:"卖五申报"`
|
||||
|
||||
//以下是字段值需二次计算
|
||||
ChangePercent float64 `json:"changePercent"` //涨跌幅
|
||||
ChangePrice float64 `json:"changePrice"` //涨跌额
|
||||
HighRate float64 `json:"highRate"` //最高涨跌
|
||||
LowRate float64 `json:"lowRate"` //最低涨跌
|
||||
CostPrice float64 `json:"costPrice"` //成本价
|
||||
CostVolume int64 `json:"costVolume"` //持仓数量
|
||||
Profit float64 `json:"profit"` //总盈亏率
|
||||
ProfitAmount float64 `json:"profitAmount"` //总盈亏金额
|
||||
ProfitAmountToday float64 `json:"profitAmountToday"` //今日盈亏金额
|
||||
|
||||
Sort int64 `json:"sort"` //排序
|
||||
AlarmChangePercent float64 `json:"alarmChangePercent"`
|
||||
AlarmPrice float64 `json:"alarmPrice"`
|
||||
}
|
||||
|
||||
func (receiver StockInfo) TableName() string {
|
||||
@@ -125,15 +146,18 @@ type StockBasic struct {
|
||||
}
|
||||
|
||||
type FollowedStock struct {
|
||||
StockCode string
|
||||
Name string
|
||||
Volume int64
|
||||
CostPrice float64
|
||||
Price float64
|
||||
PriceChange float64
|
||||
ChangePercent float64
|
||||
Time time.Time
|
||||
Sort int64
|
||||
StockCode string
|
||||
Name string
|
||||
Volume int64
|
||||
CostPrice float64
|
||||
Price float64
|
||||
PriceChange float64
|
||||
ChangePercent float64
|
||||
AlarmChangePercent float64
|
||||
AlarmPrice float64
|
||||
Time time.Time
|
||||
Sort int64
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver FollowedStock) TableName() string {
|
||||
@@ -158,20 +182,68 @@ func (receiver StockBasic) TableName() string {
|
||||
func NewStockDataApi() *StockDataApi {
|
||||
return &StockDataApi{
|
||||
client: resty.New(),
|
||||
config: getConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetIndexBasic 获取指数信息
|
||||
func (receiver StockDataApi) GetIndexBasic() {
|
||||
res := &TushareStockBasicResponse{}
|
||||
fields := "ts_code,name,market,publisher,category,base_date,base_point,list_date,fullname,index_type,weight_rule,desc"
|
||||
_, err := receiver.client.R().
|
||||
SetHeader("content-type", "application/json").
|
||||
SetBody(&TushareRequest{
|
||||
ApiName: "index_basic",
|
||||
Token: receiver.config.TushareToken,
|
||||
Params: nil,
|
||||
Fields: fields}).
|
||||
SetResult(res).
|
||||
Post(tushareApiUrl)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
if res.Code != 0 {
|
||||
logger.SugaredLogger.Error(res.Msg)
|
||||
return
|
||||
}
|
||||
//ioutil.WriteFile("index_basic.json", resp.Body(), 0666)
|
||||
|
||||
for _, item := range res.Data.Items {
|
||||
data := map[string]any{}
|
||||
for _, field := range strings.Split(fields, ",") {
|
||||
idx := slice.IndexOf(res.Data.Fields, field)
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
data[field] = item[idx]
|
||||
}
|
||||
index := &IndexBasic{}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
err := json.Unmarshal(jsonData, index)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
db.Dao.Model(&IndexBasic{}).FirstOrCreate(index, &IndexBasic{TsCode: index.TsCode}).Where("ts_code = ?", index.TsCode).Updates(index)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// map转换为结构体
|
||||
|
||||
func (receiver StockDataApi) GetStockBaseInfo() {
|
||||
res := &TushareStockBasicResponse{}
|
||||
fields := "ts_code,symbol,name,area,industry,cnspell,market,list_date,act_name,act_ent_type,fullname,exchange,list_status,curr_type,enname,delist_date,is_hs"
|
||||
_, err := receiver.client.R().
|
||||
SetHeader("content-type", "application/json").
|
||||
SetBody(&TushareRequest{
|
||||
ApiName: "stock_basic",
|
||||
Token: TushareToken,
|
||||
Token: receiver.config.TushareToken,
|
||||
Params: nil,
|
||||
Fields: "*",
|
||||
Fields: fields,
|
||||
}).
|
||||
SetResult(res).
|
||||
Post(tushare_api_url)
|
||||
Post(tushareApiUrl)
|
||||
//logger.SugaredLogger.Infof("GetStockBaseInfo %s", string(resp.Body()))
|
||||
//resp.Body()写入文件
|
||||
//ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
|
||||
@@ -185,65 +257,83 @@ func (receiver StockDataApi) GetStockBaseInfo() {
|
||||
return
|
||||
}
|
||||
for _, item := range res.Data.Items {
|
||||
ID, _ := convertor.ToInt(item[6])
|
||||
stock := &StockBasic{}
|
||||
stock.Exchange = convertor.ToString(item[0])
|
||||
stock.IsHs = convertor.ToString(item[1])
|
||||
stock.Name = convertor.ToString(item[2])
|
||||
stock.Industry = convertor.ToString(item[3])
|
||||
stock.ListStatus = convertor.ToString(item[4])
|
||||
stock.ActName = convertor.ToString(item[5])
|
||||
stock.ID = uint(ID)
|
||||
stock.CurrType = convertor.ToString(item[7])
|
||||
stock.Area = convertor.ToString(item[8])
|
||||
stock.ListDate = convertor.ToString(item[9])
|
||||
stock.DelistDate = convertor.ToString(item[10])
|
||||
stock.ActEntType = convertor.ToString(item[11])
|
||||
stock.TsCode = convertor.ToString(item[12])
|
||||
stock.Symbol = convertor.ToString(item[13])
|
||||
stock.Cnspell = convertor.ToString(item[14])
|
||||
stock.Fullname = convertor.ToString(item[20])
|
||||
stock.Ename = convertor.ToString(item[21])
|
||||
db.Dao.Model(&StockBasic{}).FirstOrCreate(stock, &StockBasic{TsCode: stock.TsCode}).Updates(stock)
|
||||
data := map[string]any{}
|
||||
for _, field := range strings.Split(fields, ",") {
|
||||
logger.SugaredLogger.Infof("field: %s", field)
|
||||
idx := slice.IndexOf(res.Data.Fields, field)
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
data[field] = item[idx]
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
err := json.Unmarshal(jsonData, stock)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
db.Dao.Model(&StockBasic{}).FirstOrCreate(stock, &StockBasic{TsCode: stock.TsCode}).Where("ts_code = ?", stock.TsCode).Updates(stock)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCode string) (*StockInfo, error) {
|
||||
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]StockInfo, error) {
|
||||
resp, err := receiver.client.R().
|
||||
SetHeader("Host", "hq.sinajs.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(fmt.Sprintf(sina_stook_url, time.Now().Unix(), StockCode))
|
||||
Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), slice.Join(StockCodes, ",")))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &StockInfo{}, nil
|
||||
return &[]StockInfo{}, err
|
||||
}
|
||||
stockData, err := ParseFullSingleStockData(GB18030ToUTF8(resp.Body()))
|
||||
var count int64
|
||||
db.Dao.Model(&StockInfo{}).Where("code = ?", StockCode).Count(&count)
|
||||
if count == 0 {
|
||||
go db.Dao.Model(&StockInfo{}).Create(stockData)
|
||||
} else {
|
||||
go db.Dao.Model(&StockInfo{}).Where("code = ?", StockCode).Updates(stockData)
|
||||
|
||||
stockInfos := make([]StockInfo, 0)
|
||||
str := GB18030ToUTF8(resp.Body())
|
||||
dataStr := strutil.SplitEx(str, "\n", true)
|
||||
for _, data := range dataStr {
|
||||
//logger.SugaredLogger.Info(data)
|
||||
stockData, err := ParseFullSingleStockData(data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
stockInfos = append(stockInfos, *stockData)
|
||||
|
||||
go func() {
|
||||
var count int64
|
||||
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Count(&count)
|
||||
if count == 0 {
|
||||
db.Dao.Model(&StockInfo{}).Create(stockData)
|
||||
} else {
|
||||
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Updates(stockData)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
return stockData, err
|
||||
|
||||
return &stockInfos, err
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) Follow(stockCode string) string {
|
||||
stockInfo, err := receiver.GetStockCodeRealTimeData(stockCode)
|
||||
logger.SugaredLogger.Infof("Follow %s", stockCode)
|
||||
stockInfos, err := receiver.GetStockCodeRealTimeData(stockCode)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "关注失败"
|
||||
}
|
||||
stockInfo := (*stockInfos)[0]
|
||||
price, _ := convertor.ToFloat(stockInfo.Price)
|
||||
db.Dao.Model(&FollowedStock{}).FirstOrCreate(&FollowedStock{
|
||||
StockCode: stockCode,
|
||||
Name: stockInfo.Name,
|
||||
Price: price,
|
||||
Time: time.Now(),
|
||||
ChangePercent: 0,
|
||||
PriceChange: 0,
|
||||
StockCode: stockCode,
|
||||
Name: stockInfo.Name,
|
||||
Price: price,
|
||||
Time: time.Now(),
|
||||
ChangePercent: 0,
|
||||
PriceChange: 0,
|
||||
Sort: 0,
|
||||
AlarmChangePercent: 3,
|
||||
AlarmPrice: price + 1,
|
||||
}, &FollowedStock{StockCode: stockCode})
|
||||
return "关注成功"
|
||||
}
|
||||
@@ -262,6 +352,22 @@ func (receiver StockDataApi) SetCostPriceAndVolume(price float64, volume int64,
|
||||
return "设置成功"
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
|
||||
err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", stockCode).Updates(&map[string]any{
|
||||
"alarm_change_percent": val,
|
||||
"alarm_price": alarmPrice,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "设置失败"
|
||||
}
|
||||
return "设置成功"
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) SetStockSort(sort int64, stockCode string) {
|
||||
db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", stockCode).Update("sort", sort)
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetFollowList() []FollowedStock {
|
||||
var result []FollowedStock
|
||||
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)
|
||||
@@ -271,13 +377,27 @@ func (receiver StockDataApi) GetFollowList() []FollowedStock {
|
||||
func (receiver StockDataApi) GetStockList(key string) []StockBasic {
|
||||
var result []StockBasic
|
||||
db.Dao.Model(&StockBasic{}).Where("name like ? or ts_code like ?", "%"+key+"%", "%"+key+"%").Find(&result)
|
||||
var result2 []IndexBasic
|
||||
db.Dao.Model(&IndexBasic{}).Where("market in ?", []string{"SSE", "SZSE"}).Where("name like ? or ts_code like ?", "%"+key+"%", "%"+key+"%").Find(&result2)
|
||||
|
||||
for _, item := range result2 {
|
||||
result = append(result, StockBasic{
|
||||
TsCode: item.TsCode,
|
||||
Name: item.Name,
|
||||
Fullname: item.FullName,
|
||||
Symbol: item.Symbol,
|
||||
Market: item.Market,
|
||||
ListDate: item.ListDate,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GB18030 转换为 UTF8
|
||||
// GB18030ToUTF8 GB18030 转换为 UTF8
|
||||
func GB18030ToUTF8(bs []byte) string {
|
||||
reader := transform.NewReader(bytes.NewReader(bs), simplifiedchinese.GB18030.NewDecoder())
|
||||
d, err := ioutil.ReadAll(reader)
|
||||
d, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -369,3 +489,161 @@ func ParseFullSingleStockData(data string) (*StockInfo, error) {
|
||||
|
||||
return stockInfo, nil
|
||||
}
|
||||
|
||||
type IndexBasic struct {
|
||||
gorm.Model
|
||||
TsCode string `json:"ts_code" gorm:"index"`
|
||||
Symbol string `json:"symbol" gorm:"index"`
|
||||
Name string `json:"name" gorm:"index"`
|
||||
FullName string `json:"fullname"`
|
||||
IndexType string `json:"index_type"`
|
||||
IndexCategory string `json:"category"`
|
||||
Market string `json:"market"`
|
||||
ListDate string `json:"list_date"`
|
||||
BaseDate string `json:"base_date"`
|
||||
BasePoint float64 `json:"base_point"`
|
||||
Publisher string `json:"publisher"`
|
||||
WeightRule string `json:"weight_rule"`
|
||||
DESC string `json:"desc"`
|
||||
}
|
||||
|
||||
func (IndexBasic) TableName() string {
|
||||
return "tushare_index_basic"
|
||||
}
|
||||
|
||||
func SearchStockPriceInfo(stockCode string) *[]string {
|
||||
url := "https://www.cls.cn/stock?code=" + stockCode
|
||||
// 创建一个 chromedp 上下文
|
||||
ctx, cancel := chromedp.NewContext(
|
||||
context.Background(),
|
||||
)
|
||||
defer cancel()
|
||||
var htmlContent string
|
||||
|
||||
var tasks chromedp.Tasks
|
||||
tasks = append(tasks, chromedp.Navigate(url))
|
||||
tasks = append(tasks, chromedp.WaitVisible("div.quote-change-box", chromedp.ByQuery))
|
||||
tasks = append(tasks, chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
price, _ := FetchPrice(ctx)
|
||||
logger.SugaredLogger.Infof("price:%s", price)
|
||||
return nil
|
||||
}))
|
||||
tasks = append(tasks, chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery))
|
||||
|
||||
err := chromedp.Run(ctx, tasks)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]string{}
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]string{}
|
||||
}
|
||||
var messages []string
|
||||
document.Find("div.quote-text-border,span.quote-price").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
logger.SugaredLogger.Info(text)
|
||||
messages = append(messages, text)
|
||||
|
||||
})
|
||||
return &messages
|
||||
}
|
||||
func FetchPrice(ctx context.Context) (string, error) {
|
||||
var price string
|
||||
timeout := time.After(10 * time.Second) // 设置超时时间为10秒
|
||||
ticker := time.NewTicker(1 * time.Second) // 每秒尝试一次
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return "", fmt.Errorf("timeout reached while fetching price")
|
||||
case <-ticker.C:
|
||||
err := chromedp.Run(ctx, chromedp.Text("span.quote-price", &price, chromedp.BySearch))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("failed to fetch price: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.SugaredLogger.Infof("price:%s", price)
|
||||
if price != "" && validator.IsNumberStr(price) {
|
||||
return price, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func SearchStockInfo(stock, msgType string) *[]string {
|
||||
// 创建一个 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://www.cls.cn/searchPage?keyword=%s&type=%s", stock, msgType)
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(url),
|
||||
// 等待页面加载完成,可以根据需要调整等待时间
|
||||
//chromedp.Sleep(3*time.Second),
|
||||
chromedp.WaitVisible(".search-content", chromedp.ByQuery),
|
||||
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
|
||||
)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]string{}
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]string{}
|
||||
}
|
||||
var messages []string
|
||||
document.Find(".search-content").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
if strings.Contains(text, stock) {
|
||||
messages = append(messages, text)
|
||||
logger.SugaredLogger.Infof("搜索到消息-%s: %s", msgType, text)
|
||||
}
|
||||
})
|
||||
return &messages
|
||||
}
|
||||
|
||||
func SearchStockInfoByCode(stock string) *[]string {
|
||||
// 创建一个 chromedp 上下文
|
||||
ctx, cancel := chromedp.NewContext(
|
||||
context.Background(),
|
||||
chromedp.WithLogf(logger.SugaredLogger.Infof),
|
||||
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
|
||||
)
|
||||
defer cancel()
|
||||
var htmlContent string
|
||||
stock = strings.ReplaceAll(stock, "sh", "")
|
||||
stock = strings.ReplaceAll(stock, "sz", "")
|
||||
url := fmt.Sprintf("https://gushitong.baidu.com/stock/ab-%s", stock)
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(url),
|
||||
// 等待页面加载完成,可以根据需要调整等待时间
|
||||
//chromedp.Sleep(3*time.Second),
|
||||
chromedp.WaitVisible("a.news-item-link", chromedp.ByQuery),
|
||||
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
|
||||
)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]string{}
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]string{}
|
||||
}
|
||||
var messages []string
|
||||
document.Find("a.news-item-link").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
if strings.Contains(text, stock) {
|
||||
messages = append(messages, text)
|
||||
logger.SugaredLogger.Infof("搜索到消息: %s", text)
|
||||
}
|
||||
})
|
||||
return &messages
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@ package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -14,9 +19,54 @@ import (
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestGetTelegraph(t *testing.T) {
|
||||
GetTelegraphList()
|
||||
}
|
||||
|
||||
func TestGetFinancialReports(t *testing.T) {
|
||||
GetFinancialReports("sz000802")
|
||||
}
|
||||
|
||||
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")
|
||||
for _, message := range *messages {
|
||||
logger.SugaredLogger.Info(message)
|
||||
}
|
||||
|
||||
//https://www.cls.cn/stock?code=sh600745
|
||||
}
|
||||
func TestSearchStockInfoByCode(t *testing.T) {
|
||||
SearchStockInfoByCode("sh600745")
|
||||
}
|
||||
|
||||
func TestSearchStockPriceInfo(t *testing.T) {
|
||||
SearchStockPriceInfo("sh600745")
|
||||
}
|
||||
|
||||
func TestParseFullSingleStockData(t *testing.T) {
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Host", "hq.sinajs.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600859,sh600745"))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
data := GB18030ToUTF8(resp.Body())
|
||||
strs := strutil.SplitEx(data, "\n", true)
|
||||
for _, str := range strs {
|
||||
logger.SugaredLogger.Info(str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStockDataApi(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
t.Log(stockDataApi.GetStockCodeRealTimeData("sh600859"))
|
||||
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745")
|
||||
for _, data := range *datas {
|
||||
t.Log(data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStockBaseInfo(t *testing.T) {
|
||||
@@ -74,6 +124,12 @@ func TestReadFile(t *testing.T) {
|
||||
func TestFollowedList(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
t.Log(stockDataApi.GetFollowList())
|
||||
stockDataApi.GetFollowList()
|
||||
|
||||
}
|
||||
|
||||
func TestStockDataApi_GetIndexBasic(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
stockDataApi.GetIndexBasic()
|
||||
}
|
||||
|
||||
159
backend/models/models.go
Normal 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"
|
||||
}
|
||||
BIN
build/app.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
build/screenshot/alipay.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
build/screenshot/img.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
build/screenshot/img_1.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
build/screenshot/img_10.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
build/screenshot/img_11.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
build/screenshot/img_12.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
build/screenshot/img_2.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
build/screenshot/img_3.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
build/screenshot/img_5.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
build/screenshot/img_6.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
build/screenshot/img_7.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
build/screenshot/img_8.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
build/screenshot/img_9.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
build/screenshot/wxpay.jpg
Normal file
|
After Width: | Height: | Size: 143 KiB |
@@ -5,19 +5,19 @@
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "{{.Name}}"
|
||||
!define INFO_PROJECTNAME "go-stock"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||
!define INFO_COMPANYNAME "sparkmemory"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||
!define INFO_PRODUCTNAME "go-stock"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||
!define INFO_PRODUCTVERSION "1.0.0"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||
!define INFO_COPYRIGHT "Copyright#sparkmemory@163.com"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
@@ -203,20 +203,12 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!macro wails.associateFiles
|
||||
; Create file associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
File "..\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateFiles
|
||||
; Delete app associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||
|
||||
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
@@ -235,15 +227,10 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
{{end}}
|
||||
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||
{{end}}
|
||||
|
||||
!macroend
|
||||
|
||||
760
frontend/package-lock.json
generated
@@ -8,11 +8,14 @@
|
||||
"name": "go-stock",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"vue": "^3.2.25"
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"md-editor-v3": "^5.2.1",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"naive-ui": "^2.40.3",
|
||||
"naive-ui": "^2.41.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vite": "5.4.6"
|
||||
}
|
||||
@@ -59,6 +62,360 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.18.4",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz",
|
||||
"integrity": "sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.8.0.tgz",
|
||||
"integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-angular": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-angular/-/lang-angular-0.1.3.tgz",
|
||||
"integrity": "sha512-xgeWGJQQl1LyStvndWtruUvb4SnBZDAu/gvFH/ZU+c0W25tQR8e5hq7WTwiIY2dNxnf+49mRiGI/9yxIwB6f5w==",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-cpp": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz",
|
||||
"integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/cpp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/css": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-go": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
|
||||
"integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/go": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
|
||||
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/css": "^1.1.0",
|
||||
"@lezer/html": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-java": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-java/-/lang-java-6.0.1.tgz",
|
||||
"integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/java": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
|
||||
"integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
|
||||
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-less": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-less/-/lang-less-6.0.2.tgz",
|
||||
"integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.2.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-liquid": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-liquid/-/lang-liquid-6.2.2.tgz",
|
||||
"integrity": "sha512-7Dm841fk37+JQW6j2rI1/uGkJyESrjzyhiIkaLjbbR0U6aFFQvMrJn35WxQreRMADMhzkyVkZM4467OR7GR8nQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-markdown/-/lang-markdown-6.3.2.tgz",
|
||||
"integrity": "sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-php": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-php/-/lang-php-6.0.1.tgz",
|
||||
"integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/php": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-python": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-python/-/lang-python-6.1.6.tgz",
|
||||
"integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.3.2",
|
||||
"@codemirror/language": "^6.8.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/python": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-rust": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz",
|
||||
"integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/rust": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-sass": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz",
|
||||
"integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.2.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/sass": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-sql": {
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz",
|
||||
"integrity": "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-vue": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz",
|
||||
"integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-wast": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz",
|
||||
"integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-xml": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
|
||||
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/xml": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-yaml": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
|
||||
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"@lezer/yaml": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.10.8",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.10.8.tgz",
|
||||
"integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.1.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language-data": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/language-data/-/language-data-6.5.1.tgz",
|
||||
"integrity": "sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-angular": "^0.1.0",
|
||||
"@codemirror/lang-cpp": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-go": "^6.0.0",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/lang-java": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/lang-json": "^6.0.0",
|
||||
"@codemirror/lang-less": "^6.0.0",
|
||||
"@codemirror/lang-liquid": "^6.0.0",
|
||||
"@codemirror/lang-markdown": "^6.0.0",
|
||||
"@codemirror/lang-php": "^6.0.0",
|
||||
"@codemirror/lang-python": "^6.0.0",
|
||||
"@codemirror/lang-rust": "^6.0.0",
|
||||
"@codemirror/lang-sass": "^6.0.0",
|
||||
"@codemirror/lang-sql": "^6.0.0",
|
||||
"@codemirror/lang-vue": "^0.1.1",
|
||||
"@codemirror/lang-wast": "^6.0.0",
|
||||
"@codemirror/lang-xml": "^6.0.0",
|
||||
"@codemirror/lang-yaml": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/legacy-modes": "^6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/legacy-modes": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.4.2.tgz",
|
||||
"integrity": "sha512-HsvWu08gOIIk303eZQCal4H4t65O/qp1V4ul4zVa3MHK5FJ0gz3qz3O55FIkm+aQUcshUOjBx38t2hPiJwW5/g==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.8.4",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.4.tgz",
|
||||
"integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.5.8",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.8.tgz",
|
||||
"integrity": "sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.1.tgz",
|
||||
"integrity": "sha512-3rA9lcwciEB47ZevqvD8qgbzhM9qMb8vCcQCNmDfVRPQG4JT9mSb0Jg8H7YjKGGQcFnLN323fj9jdnG59Kx6bg==",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.36.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.36.2.tgz",
|
||||
"integrity": "sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@css-render/plugin-bem": {
|
||||
"version": "0.15.14",
|
||||
"resolved": "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz",
|
||||
@@ -462,6 +819,171 @@
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.2.3.tgz",
|
||||
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
|
||||
},
|
||||
"node_modules/@lezer/cpp": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/cpp/-/cpp-1.1.2.tgz",
|
||||
"integrity": "sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/css/-/css-1.1.9.tgz",
|
||||
"integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/go": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/go/-/go-1.0.0.tgz",
|
||||
"integrity": "sha512-co9JfT3QqX1YkrMmourYw2Z8meGC50Ko4d54QEcQbEYpvdUvN4yb0NBZdn/9ertgvjsySxHsKzH3lbm3vqJ4Jw==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.1.tgz",
|
||||
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/html/-/html-1.3.10.tgz",
|
||||
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/java": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/java/-/java-1.1.3.tgz",
|
||||
"integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.4.21",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.4.21.tgz",
|
||||
"integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.2.tgz",
|
||||
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.4.0.tgz",
|
||||
"integrity": "sha512-mk4MYeq6ZQdxgsgRAe0G7kqPRV6Desajfa14TcHoGGXIqqj1/2ARN31VFpmrXDgvXiGBWpA7RXtv0he+UdTkGw==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/php": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/php/-/php-1.0.2.tgz",
|
||||
"integrity": "sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/python": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/python/-/python-1.1.15.tgz",
|
||||
"integrity": "sha512-aVQ43m2zk4FZYedCqL0KHPEUsqZOrmAvRhkhHlVPnDD1HODDyyQv5BRIuod4DadkgBEZd53vQOtXTonNbEgjrQ==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/rust": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/rust/-/rust-1.0.2.tgz",
|
||||
"integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/sass": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/sass/-/sass-1.0.7.tgz",
|
||||
"integrity": "sha512-8HLlOkuX/SMHOggI2DAsXUw38TuURe+3eQ5hiuk9QmYOUyC55B1dYEIMkav5A4IELVaW4e1T4P9WRiI5ka4mdw==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/xml": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/xml/-/xml-1.0.6.tgz",
|
||||
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/yaml": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.3.tgz",
|
||||
"integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
|
||||
@@ -721,6 +1243,11 @@
|
||||
"integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.13.tgz",
|
||||
@@ -736,6 +1263,30 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
|
||||
},
|
||||
"node_modules/@vavt/util": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vavt/util/-/util-2.1.0.tgz",
|
||||
"integrity": "sha512-YIfAvArSFVXmWvoF+DEGD0FhkhVNcCtVWWkfYtj76eSrwHh/wuEEFhiEubg1XLNM3tChO8FH8xJCT/hnizjgFQ=="
|
||||
},
|
||||
"node_modules/@vicons/ionicons5": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz",
|
||||
"integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ=="
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
|
||||
@@ -795,6 +1346,11 @@
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
||||
@@ -840,12 +1396,49 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz",
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.1.tgz",
|
||||
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/copy-to-clipboard": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
|
||||
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
|
||||
"dependencies": {
|
||||
"toggle-selection": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
|
||||
},
|
||||
"node_modules/css-render": {
|
||||
"version": "0.15.14",
|
||||
"resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz",
|
||||
@@ -862,6 +1455,11 @@
|
||||
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cssfilter": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/cssfilter/-/cssfilter-0.0.10.tgz",
|
||||
"integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -969,6 +1567,14 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -981,6 +1587,22 @@
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.0.2.tgz",
|
||||
"integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-vue-next": {
|
||||
"version": "0.453.0",
|
||||
"resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.453.0.tgz",
|
||||
"integrity": "sha512-5zmv83vxAs9SVoe22veDBi8Dw0Fh2F+oTngWgKnKOkrZVbZjceXLQ3tescV2boB0zlaf9R2Sd9RuUP2766xvsQ==",
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.14",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.14.tgz",
|
||||
@@ -989,10 +1611,81 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz",
|
||||
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it-image-figures": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/markdown-it-image-figures/-/markdown-it-image-figures-2.1.1.tgz",
|
||||
"integrity": "sha512-mwXSQ2nPeVUzCMIE3HlLvjRioopiqyJLNph0pyx38yf9mpqFDhNGnMpAXF9/A2Xv0oiF2cVyg9xwfF0HNAz05g==",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"markdown-it": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it-sub": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/markdown-it-sub/-/markdown-it-sub-2.0.0.tgz",
|
||||
"integrity": "sha512-iCBKgwCkfQBRg2vApy9vx1C1Tu6D8XYo8NvevI3OlwzBRmiMtsJ2sXupBgEA7PPxiDwNni3qIUkhZ6j5wofDUA=="
|
||||
},
|
||||
"node_modules/markdown-it-sup": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/markdown-it-sup/-/markdown-it-sup-2.0.0.tgz",
|
||||
"integrity": "sha512-5VgmdKlkBd8sgXuoDoxMpiU+BiEt3I49GItBzzw7Mxq9CxvnhE/k09HFli09zgfFDRixDQDfDxi0mgBCXtaTvA=="
|
||||
},
|
||||
"node_modules/md-editor-v3": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/md-editor-v3/-/md-editor-v3-5.2.1.tgz",
|
||||
"integrity": "sha512-FPbVEO/9xovXM5OULfBPdM22LrKevEHnkjmYF0pqi34UF/Wt3d5LW/UX2DehwCTmYua6N7385K93un58K69xPA==",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-markdown": "^6.3.0",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@types/markdown-it": "^14.0.1",
|
||||
"@vavt/util": "^2.1.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"lru-cache": "^11.0.1",
|
||||
"lucide-vue-next": "^0.453.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"markdown-it-image-figures": "^2.1.1",
|
||||
"markdown-it-sub": "^2.0.0",
|
||||
"markdown-it-sup": "^2.0.0",
|
||||
"medium-zoom": "^1.1.0",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
|
||||
},
|
||||
"node_modules/medium-zoom": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/medium-zoom/-/medium-zoom-1.1.0.tgz",
|
||||
"integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ=="
|
||||
},
|
||||
"node_modules/naive-ui": {
|
||||
"version": "2.40.3",
|
||||
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.40.3.tgz",
|
||||
"integrity": "sha512-TpgYfOg0SNlG4HHhTdFnFcPc1trZiX3r10Pn6biyEgRoi6ZC5qbsY8xgKsqQuG4nWj2PHLT8pPVEkt2pKOlxag==",
|
||||
"version": "2.41.0",
|
||||
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.41.0.tgz",
|
||||
"integrity": "sha512-KnmLg+xPLwXV8QVR7ZZ69eCjvel7R5vru8+eFe4VoAJHEgqAJgVph6Zno9K2IVQRpSF3GBGea3tjavslOR4FAA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@css-render/plugin-bem": "^0.15.14",
|
||||
@@ -1068,6 +1761,14 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.28.1.tgz",
|
||||
@@ -1120,12 +1821,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz",
|
||||
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
|
||||
},
|
||||
"node_modules/toggle-selection": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
|
||||
},
|
||||
"node_modules/treemate": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz",
|
||||
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
|
||||
},
|
||||
"node_modules/vdirs": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz",
|
||||
@@ -1235,6 +1951,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz",
|
||||
"integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vueuc": {
|
||||
"version": "0.4.64",
|
||||
"resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.64.tgz",
|
||||
@@ -1252,6 +1982,26 @@
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
|
||||
},
|
||||
"node_modules/xss": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmmirror.com/xss/-/xss-1.0.15.tgz",
|
||||
"integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==",
|
||||
"dependencies": {
|
||||
"commander": "^2.20.3",
|
||||
"cssfilter": "0.0.10"
|
||||
},
|
||||
"bin": {
|
||||
"xss": "bin/xss"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,17 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.25"
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"md-editor-v3": "^5.2.1",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"naive-ui": "^2.40.3",
|
||||
"naive-ui": "^2.41.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vite": "5.4.6"
|
||||
},
|
||||
"keywords": [],
|
||||
"keywords": ["AI赋能股票分析","go-stock"],
|
||||
"author": "spark"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
9ce62efac1fed08499bbf20c8a5fd1b2
|
||||
39a415166f03acc0270e24443a9e2445
|
||||
@@ -1,32 +1,213 @@
|
||||
<script setup>
|
||||
import stockInfo from './components/stock.vue'
|
||||
import {ref} from "vue";
|
||||
import {
|
||||
EventsOn,
|
||||
Quit,
|
||||
WindowFullscreen, WindowGetPosition,
|
||||
WindowHide,
|
||||
WindowIsFullscreen, WindowSetPosition,
|
||||
WindowUnfullscreen
|
||||
} from '../wailsjs/runtime'
|
||||
import {h, ref} from "vue";
|
||||
import { RouterLink } from 'vue-router'
|
||||
import {darkTheme, NIcon, NText,} from 'naive-ui'
|
||||
import {
|
||||
SettingsOutline,
|
||||
ReorderTwoOutline,
|
||||
ExpandOutline,
|
||||
RefreshOutline, PowerOutline, BarChartOutline, MoveOutline, WalletOutline, StarOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const content = ref('数据来源于网络,仅供参考\n投资有风险,入市需谨慎')
|
||||
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
const isFullscreen = ref(false)
|
||||
const activeKey = ref('stock')
|
||||
const containerRef= ref({})
|
||||
const realtimeProfit= ref(0)
|
||||
const telegraph= ref([])
|
||||
const menuOptions = ref([
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'stock',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
},
|
||||
}
|
||||
},
|
||||
{ default: () => '我的自选',}
|
||||
),
|
||||
key: 'stock',
|
||||
icon: renderIcon(StarOutline),
|
||||
children:[
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(WalletOutline),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'settings',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
}
|
||||
}
|
||||
},
|
||||
{ default: () => '设置' }
|
||||
),
|
||||
key: 'settings',
|
||||
icon: renderIcon(SettingsOutline),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: '#',
|
||||
onClick: toggleFullscreen,
|
||||
title: '全屏 Ctrl+F 退出全屏 Esc',
|
||||
}, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
|
||||
key: 'full',
|
||||
icon: renderIcon(ExpandOutline),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: '#',
|
||||
onClick: WindowHide,
|
||||
title: '隐藏到托盘区 Ctrl+H',
|
||||
}, { default: () => '隐藏到托盘区' }),
|
||||
key: 'hide',
|
||||
icon: renderIcon(ReorderTwoOutline),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: 'javascript:void(0)',
|
||||
style: 'cursor: move;',
|
||||
onClick: toggleStartMoveWindow,
|
||||
}, { default: () => '移动' }),
|
||||
key: 'move',
|
||||
icon: renderIcon(MoveOutline),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: '#',
|
||||
onClick: Quit,
|
||||
}, { default: () => '退出程序' }),
|
||||
key: 'exit',
|
||||
icon: renderIcon(PowerOutline),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'about',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
}
|
||||
}
|
||||
},
|
||||
{ default: () => '关于软件' }
|
||||
),
|
||||
key: 'about',
|
||||
icon: renderIcon(StarOutline),
|
||||
},
|
||||
])
|
||||
function renderIcon(icon) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) })
|
||||
}
|
||||
function toggleFullscreen(e) {
|
||||
//console.log(e)
|
||||
if (isFullscreen.value) {
|
||||
WindowUnfullscreen()
|
||||
//e.target.innerHTML = '全屏'
|
||||
} else {
|
||||
WindowFullscreen()
|
||||
// e.target.innerHTML = '取消全屏'
|
||||
}
|
||||
isFullscreen.value=!isFullscreen.value
|
||||
}
|
||||
const drag = ref(false)
|
||||
const lastPos= ref({x:0,y:0})
|
||||
function toggleStartMoveWindow(e) {
|
||||
drag.value=!drag.value
|
||||
lastPos.value={x:e.clientX,y:e.clientY}
|
||||
}
|
||||
function dragstart(e) {
|
||||
if (drag.value) {
|
||||
let x=e.clientX-lastPos.value.x
|
||||
let y=e.clientY-lastPos.value.y
|
||||
WindowGetPosition().then((pos) => {
|
||||
WindowSetPosition(pos.x+x,pos.y+y)
|
||||
})
|
||||
}
|
||||
}
|
||||
window.addEventListener('mousemove', dragstart)
|
||||
|
||||
EventsOn("realtime_profit",(data)=>{
|
||||
realtimeProfit.value=data
|
||||
})
|
||||
EventsOn("telegraph",(data)=>{
|
||||
telegraph.value=data
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
|
||||
<n-config-provider :theme="darkTheme" ref="containerRef">
|
||||
<n-message-provider >
|
||||
<n-notification-provider>
|
||||
<n-modal-provider>
|
||||
<n-watermark
|
||||
:content="content"
|
||||
cross
|
||||
selectable
|
||||
:font-size="12"
|
||||
:line-height="12"
|
||||
:font-size="16"
|
||||
:line-height="16"
|
||||
:width="500"
|
||||
:height="400"
|
||||
:width="200"
|
||||
:x-offset="50"
|
||||
:y-offset="50"
|
||||
:y-offset="150"
|
||||
:rotate="-15"
|
||||
style="height: 100%"
|
||||
>
|
||||
<n-flex justify="center">
|
||||
<n-message-provider >
|
||||
<n-modal-provider>
|
||||
<stockInfo/>
|
||||
</n-modal-provider>
|
||||
</n-message-provider>
|
||||
<n-grid x-gap="12" :cols="1">
|
||||
<n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
|
||||
<n-marquee :speed="120" >
|
||||
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
|
||||
{{item}}
|
||||
</n-tag>
|
||||
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
|
||||
</n-marquee>
|
||||
</n-gi>
|
||||
<n-gi style="padding-bottom: 70px;padding-top: 5px">
|
||||
<RouterView />
|
||||
</n-gi>
|
||||
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%">
|
||||
<n-card size="small">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
responsive
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
</n-flex>
|
||||
</n-watermark>
|
||||
</n-modal-provider>
|
||||
</n-notification-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
<style>
|
||||
|
||||
|
||||
87
frontend/src/components/about.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<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(`
|
||||
feat(frontend): 添加关于软件页面
|
||||
|
||||
- 在 App.vue 中添加关于软件的菜单项
|
||||
- 在 router.js 中添加关于软件的路由- 新增 about.vue 组件,包含软件介绍和作者信息
|
||||
`)
|
||||
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>
|
||||
<n-image width="100" :src="icon" />
|
||||
<h1>go-stock <n-tag size="small" round>{{versionInfo}}</n-tag></h1>
|
||||
<p>自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
|
||||
<p>
|
||||
欢迎点赞GitHub:<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">go-stock</a>
|
||||
</p>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- 更新说明 -->
|
||||
<n-card size="large">
|
||||
<n-flex justify="center">
|
||||
<h1>更新说明</h1>
|
||||
<MdPreview style="text-align: left" :modelValue="updateLog" :theme="'dark'"/>
|
||||
</n-flex>
|
||||
|
||||
</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>
|
||||
</n-space>
|
||||
</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>
|
||||
200
frontend/src/components/settings.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
|
||||
import {useMessage} from "naive-ui";
|
||||
import {data} from "../../wailsjs/go/models";
|
||||
const message = useMessage()
|
||||
|
||||
const formRef = ref(null)
|
||||
const formValue = ref({
|
||||
ID:1,
|
||||
tushareToken:'',
|
||||
dingPush:{
|
||||
enable:false,
|
||||
dingRobot: ''
|
||||
},
|
||||
localPush:{
|
||||
enable:true,
|
||||
},
|
||||
updateBasicInfoOnStart:false,
|
||||
refreshInterval:1,
|
||||
openAI:{
|
||||
enable:false,
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
apiKey: '',
|
||||
model: 'deepseek-chat',
|
||||
temperature: 0.1,
|
||||
maxTokens: 1024,
|
||||
prompt:"",
|
||||
timeout: 5
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(()=>{
|
||||
GetConfig().then(res=>{
|
||||
formValue.value.ID = res.ID
|
||||
formValue.value.tushareToken = res.tushareToken
|
||||
formValue.value.dingPush = {
|
||||
enable:res.dingPushEnable,
|
||||
dingRobot:res.dingRobot
|
||||
}
|
||||
formValue.value.localPush = {
|
||||
enable:res.localPushEnable,
|
||||
}
|
||||
formValue.value.updateBasicInfoOnStart = res.updateBasicInfoOnStart
|
||||
formValue.value.refreshInterval = res.refreshInterval
|
||||
formValue.value.openAI = {
|
||||
enable:res.openAiEnable,
|
||||
baseUrl: res.openAiBaseUrl,
|
||||
apiKey:res.openAiApiKey,
|
||||
model:res.openAiModelName,
|
||||
temperature:res.openAiTemperature,
|
||||
maxTokens:res.openAiMaxTokens,
|
||||
prompt:res.prompt,
|
||||
timeout:res.openAiApiTimeOut
|
||||
}
|
||||
console.log(res)
|
||||
})
|
||||
//message.info("加载完成")
|
||||
})
|
||||
|
||||
|
||||
function saveConfig(){
|
||||
let config= new data.Settings({
|
||||
ID:formValue.value.ID,
|
||||
dingPushEnable:formValue.value.dingPush.enable,
|
||||
dingRobot:formValue.value.dingPush.dingRobot,
|
||||
localPushEnable:formValue.value.localPush.enable,
|
||||
updateBasicInfoOnStart:formValue.value.updateBasicInfoOnStart,
|
||||
refreshInterval:formValue.value.refreshInterval,
|
||||
openAiEnable:formValue.value.openAI.enable,
|
||||
openAiBaseUrl:formValue.value.openAI.baseUrl,
|
||||
openAiApiKey:formValue.value.openAI.apiKey,
|
||||
openAiModelName:formValue.value.openAI.model,
|
||||
openAiMaxTokens:formValue.value.openAI.maxTokens,
|
||||
openAiTemperature:formValue.value.openAI.temperature,
|
||||
tushareToken:formValue.value.tushareToken,
|
||||
prompt:formValue.value.openAI.prompt,
|
||||
openAiApiTimeOut:formValue.value.openAI.timeout
|
||||
})
|
||||
|
||||
//console.log("Settings",config)
|
||||
UpdateConfig(config).then(res=>{
|
||||
message.success(res)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function getHeight() {
|
||||
return document.documentElement.clientHeight
|
||||
}
|
||||
function sendTestNotice(){
|
||||
let markdown="### go-stock test\n"+new Date()
|
||||
let msg='{' +
|
||||
' "msgtype": "markdown",' +
|
||||
' "markdown": {' +
|
||||
' "title":"go-stock'+new Date()+'",' +
|
||||
' "text": "'+markdown+'"' +
|
||||
' },' +
|
||||
' "at": {' +
|
||||
' "isAtAll": true' +
|
||||
' }' +
|
||||
' }'
|
||||
|
||||
SendDingDingMessageByType(msg, "test-"+new Date().getTime(),1).then(res=>{
|
||||
message.info(res)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px">
|
||||
<n-form ref="formRef" :model="formValue" :label-placement="'left'" :label-align="'left'" style="width: 100%;height: 100%">
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<n-gi :span="24">
|
||||
<n-text type="default" style="font-size: 25px;font-weight: bold">基础设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="10" label="Tushare api token:" path="tushareToken" >
|
||||
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
|
||||
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="数据刷新间隔(重启生效):" path="refreshInterval" >
|
||||
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
|
||||
<template #suffix>
|
||||
秒
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<n-gi :span="24">
|
||||
<n-text type="default" style="font-size: 25px;font-weight: bold">通知设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" >
|
||||
<n-switch v-model:value="formValue.dingPush.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="是否启用本地推送:" path="localPush.enable" >
|
||||
<n-switch v-model:value="formValue.localPush.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
|
||||
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
|
||||
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
|
||||
<n-gi :span="24">
|
||||
<n-text type="default" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
|
||||
</n-gi>
|
||||
<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="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="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="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="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="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-input v-model:value="formValue.openAI.prompt"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入系统prompt"
|
||||
:autosize="{
|
||||
minRows: 5,
|
||||
maxRows: 8
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-gi :span="24">
|
||||
<div style="display: flex; justify-content: center">
|
||||
<n-button type="primary" @click="saveConfig">
|
||||
保存
|
||||
</n-button>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-form>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,10 +1,36 @@
|
||||
<script setup>
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
|
||||
import {Greet, Follow, UnFollow, GetFollowList, GetStockList, SetCostPriceAndVolume} from '../../wailsjs/go/main/App'
|
||||
import {NButton, NFlex, NForm, NFormItem, NInput, NInputNumber, NText, useMessage, useModal} from 'naive-ui'
|
||||
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
|
||||
import {
|
||||
Follow, GetConfig,
|
||||
GetFollowList,
|
||||
GetStockList,
|
||||
Greet, SaveAIResponseResult, NewChatStream,
|
||||
SendDingDingMessage, SendDingDingMessageByType,
|
||||
SetAlarmChangePercent,
|
||||
SetCostPriceAndVolume, SetStockSort,
|
||||
UnFollow, GetAIResponseResult
|
||||
} from '../../wailsjs/go/main/App'
|
||||
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';
|
||||
// preview.css相比style.css少了编辑器那部分样式
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
const mdPreviewRef = ref(null)
|
||||
const message = useMessage()
|
||||
const modal = useModal()
|
||||
const notify = useNotification()
|
||||
|
||||
const stocks=ref([])
|
||||
const results=ref({})
|
||||
@@ -13,26 +39,48 @@ const stockList=ref([])
|
||||
const followList=ref([])
|
||||
const options=ref([])
|
||||
const modalShow = ref(false)
|
||||
const modalShow2 = ref(false)
|
||||
const modalShow3 = ref(false)
|
||||
const modalShow4 = ref(false)
|
||||
const addBTN = ref(true)
|
||||
const formModel = ref({
|
||||
name: "",
|
||||
code: "",
|
||||
costPrice: 0.000,
|
||||
volume: 0
|
||||
volume: 0,
|
||||
alarm: 0,
|
||||
alarmPrice:0,
|
||||
sort:999,
|
||||
})
|
||||
|
||||
const data = reactive({
|
||||
name: "",
|
||||
code: "",
|
||||
fenshiURL:"",
|
||||
kURL:"",
|
||||
resultText: "Please enter your name below 👇",
|
||||
fullscreen: false,
|
||||
airesult: "",
|
||||
openAiEnable: false,
|
||||
loading: true,
|
||||
})
|
||||
|
||||
const sortedResults = computed(() => {
|
||||
//console.log("computed",sortedResults.value)
|
||||
const sortedKeys =Object.keys(results.value).sort();
|
||||
const sortedObject = {};
|
||||
sortedKeys.forEach(key => {
|
||||
sortedObject[key] = results.value[key];
|
||||
});
|
||||
return sortedObject
|
||||
});
|
||||
|
||||
onBeforeMount(()=>{
|
||||
GetStockList("").then(result => {
|
||||
stockList.value = result
|
||||
options.value=result.map(item => {
|
||||
return {
|
||||
label: item.name+" "+item.ts_code,
|
||||
label: item.name+" - "+item.ts_code,
|
||||
value: item.ts_code
|
||||
}
|
||||
})
|
||||
@@ -45,27 +93,122 @@ onBeforeMount(()=>{
|
||||
}
|
||||
}
|
||||
monitor()
|
||||
message.destroyAll
|
||||
message.destroyAll()
|
||||
})
|
||||
GetConfig().then(result => {
|
||||
if (result.openAiEnable) {
|
||||
data.openAiEnable = true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
message.loading("Loading...")
|
||||
console.log(`the component is now mounted.`)
|
||||
// console.log(`the component is now mounted.`)
|
||||
|
||||
ticker.value=setInterval(() => {
|
||||
if(isTradingTime()){
|
||||
monitor()
|
||||
//monitor()
|
||||
data.fenshiURL='http://image.sinajs.cn/newchart/min/n/'+data.code+'.gif'+"?t="+Date.now()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
}, 3500)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
console.log(`the component is now unmounted.`)
|
||||
// console.log(`the component is now unmounted.`)
|
||||
clearInterval(ticker.value)
|
||||
})
|
||||
|
||||
EventsOn("refresh",(data)=>{
|
||||
message.success(data)
|
||||
})
|
||||
|
||||
EventsOn("showSearch",(data)=>{
|
||||
addBTN.value = data === 1;
|
||||
})
|
||||
|
||||
EventsOn("stock_price",(data)=>{
|
||||
//console.log("stock_price",data['股票代码'])
|
||||
updateData(data)
|
||||
})
|
||||
|
||||
EventsOn("refreshFollowList",(data)=>{
|
||||
|
||||
WindowReload()
|
||||
// message.loading("refresh...")
|
||||
// GetFollowList().then(result => {
|
||||
// followList.value = result
|
||||
// for (const followedStock of result) {
|
||||
// if (!stocks.value.includes(followedStock.StockCode)) {
|
||||
// stocks.value.push(followedStock.StockCode)
|
||||
// }
|
||||
// }
|
||||
// monitor()
|
||||
// message.destroyAll
|
||||
// })
|
||||
})
|
||||
|
||||
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 {
|
||||
data.airesult = data.airesult + 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股交易时间
|
||||
function isTradingTime() {
|
||||
@@ -93,92 +236,163 @@ function AddStock(){
|
||||
Follow(data.code).then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
monitor()
|
||||
}else{
|
||||
message.error("已经关注了")
|
||||
}
|
||||
monitor()
|
||||
}
|
||||
|
||||
|
||||
|
||||
function removeMonitor(code,name) {
|
||||
console.log("removeMonitor",name,code)
|
||||
function removeMonitor(code,name,key) {
|
||||
console.log("removeMonitor",name,code,key)
|
||||
stocks.value.splice(stocks.value.indexOf(code),1)
|
||||
delete results.value[name]
|
||||
console.log("removeMonitor-key",key)
|
||||
console.log("removeMonitor-v",results.value[key])
|
||||
|
||||
delete results.value[key]
|
||||
console.log("removeMonitor-v",results.value[key])
|
||||
|
||||
UnFollow(code).then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
}
|
||||
|
||||
function getStockList(){
|
||||
function getStockList(value){
|
||||
// console.log("getStockList",value)
|
||||
let result;
|
||||
result=stockList.value.filter(item => item.name.includes(data.name)||item.ts_code.includes(data.name))
|
||||
result=stockList.value.filter(item => item.name.includes(value)||item.ts_code.includes(value))
|
||||
options.value=result.map(item => {
|
||||
return {
|
||||
label: item.name+" "+item.ts_code,
|
||||
label: item.name+" - "+item.ts_code,
|
||||
value: item.ts_code
|
||||
}
|
||||
})
|
||||
if(value&&value.indexOf("-")<=0){
|
||||
data.code=value
|
||||
}
|
||||
}
|
||||
|
||||
function monitor() {
|
||||
async function updateData(result) {
|
||||
if(result["当前价格"]<=0){
|
||||
result["当前价格"]=result["卖一报价"]
|
||||
}
|
||||
|
||||
if (result.changePercent>0) {
|
||||
result.type="error"
|
||||
result.color="#E88080"
|
||||
}else if (result.changePercent<0) {
|
||||
result.type="success"
|
||||
result.color="#63E2B7"
|
||||
}else {
|
||||
result.type="default"
|
||||
result.color="#FFFFFF"
|
||||
}
|
||||
|
||||
if(result.profitAmount>0){
|
||||
result.profitType="error"
|
||||
}else if(result.profitAmount<0){
|
||||
result.profitType="success"
|
||||
}
|
||||
if(result["当前价格"]){
|
||||
if(result.alarmChangePercent>0&&Math.abs(result.changePercent)>=result.alarmChangePercent){
|
||||
SendMessage(result,1)
|
||||
}
|
||||
|
||||
if(result.alarmPrice>0&&result["当前价格"]>=result.alarmPrice){
|
||||
SendMessage(result,2)
|
||||
}
|
||||
|
||||
if(result.costPrice>0&&result["当前价格"]>=result.costPrice){
|
||||
SendMessage(result,3)
|
||||
}
|
||||
}
|
||||
|
||||
result.key=GetSortKey(result.sort,result["股票代码"])
|
||||
results.value[GetSortKey(result.sort,result["股票代码"])]=result
|
||||
}
|
||||
|
||||
|
||||
async function monitor() {
|
||||
for (let code of stocks.value) {
|
||||
// console.log(code)
|
||||
Greet(code).then(result => {
|
||||
let s=(result["当前价格"]-result["昨日收盘价"])*100/result["昨日收盘价"]
|
||||
let roundedNum = s.toFixed(2); // 将数字转换为保留两位小数的字符串形式
|
||||
result.s=roundedNum+"%"
|
||||
|
||||
result.highRate=((result["今日最高价"]-result["今日开盘价"])*100/result["今日开盘价"]).toFixed(2)+"%"
|
||||
result.lowRate=((result["今日最低价"]-result["今日开盘价"])*100/result["今日开盘价"]).toFixed(2)+"%"
|
||||
|
||||
|
||||
if (roundedNum>0) {
|
||||
result.type="error"
|
||||
result.color="#E88080"
|
||||
}else if (roundedNum<0) {
|
||||
result.type="success"
|
||||
result.color="#63E2B7"
|
||||
}else {
|
||||
result.type="default"
|
||||
result.color="#FFFFFF"
|
||||
}
|
||||
let res= followList.value.filter(item => item.StockCode===code)
|
||||
if (res.length>0) {
|
||||
result.costPrice=res[0].CostPrice
|
||||
result.volume=res[0].Volume
|
||||
result.profit=((result["当前价格"]-result.costPrice)*100/result.costPrice).toFixed(3)
|
||||
result.profitAmountToday=(result.volume*(result["当前价格"]-result["昨日收盘价"])).toFixed(2)
|
||||
result.profitAmount=(result.volume*(result["当前价格"]-result.costPrice)).toFixed(2)
|
||||
if(result.profitAmount>0){
|
||||
result.profitType="error"
|
||||
}else if(result.profitAmount<0){
|
||||
result.profitType="success"
|
||||
}
|
||||
}
|
||||
results.value[result["股票名称"]]=result
|
||||
updateData(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//数字长度不够前面补0
|
||||
function padZero(num, length) {
|
||||
return (Array(length).join('0') + num).slice(-length);
|
||||
}
|
||||
|
||||
function GetSortKey(sort,code){
|
||||
return padZero(sort,6)+"_"+code
|
||||
}
|
||||
|
||||
function onSelect(item) {
|
||||
data.code=item.split(".")[1].toLowerCase()+item.split(".")[0]
|
||||
//console.log("onSelect",item)
|
||||
|
||||
if(item.indexOf("-")>0){
|
||||
item=item.split("-")[1].toLowerCase()
|
||||
}
|
||||
if(item.indexOf(".")>0){
|
||||
data.code=item.split(".")[1].toLowerCase()+item.split(".")[0]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function search(code,name){
|
||||
setTimeout(() => {
|
||||
window.open("https://xueqiu.com/S/"+code)
|
||||
//window.open("https://xueqiu.com/S/"+code)
|
||||
//window.open("https://www.cls.cn/stock?code="+code)
|
||||
//window.open("https://quote.eastmoney.com/"+code+".html")
|
||||
//window.open("https://finance.sina.com.cn/realstock/company/"+code+"/nc.shtml")
|
||||
window.open("https://www.iwencai.com/unifiedwap/result?w="+code)
|
||||
//window.open("https://www.iwencai.com/chat/?question="+code)
|
||||
}, 500)
|
||||
}
|
||||
function setStock(code,name){
|
||||
let res=followList.value.filter(item => item.StockCode===code)
|
||||
console.log("res:",res)
|
||||
//console.log("res:",res)
|
||||
formModel.value.name=name
|
||||
formModel.value.code=code
|
||||
formModel.value.volume=res[0].Volume
|
||||
formModel.value.costPrice=res[0].CostPrice
|
||||
formModel.value.alarm=res[0].AlarmChangePercent
|
||||
formModel.value.alarmPrice=res[0].AlarmPrice
|
||||
formModel.value.sort=res[0].Sort
|
||||
modalShow.value=true
|
||||
}
|
||||
|
||||
function updateCostPriceAndVolumeNew(code,price,volume){
|
||||
console.log(code,price,volume)
|
||||
function showFenshi(code,name){
|
||||
data.code=code
|
||||
data.name=name
|
||||
data.fenshiURL='http://image.sinajs.cn/newchart/min/n/'+data.code+'.gif'+"?t="+Date.now()
|
||||
modalShow2.value=true
|
||||
}
|
||||
function showK(code,name){
|
||||
data.code=code
|
||||
data.name=name
|
||||
data.kURL='http://image.sinajs.cn/newchart/daily/n/'+data.code+'.gif'+"?t="+Date.now()
|
||||
modalShow3.value=true
|
||||
}
|
||||
|
||||
|
||||
function updateCostPriceAndVolumeNew(code,price,volume,alarm,formModel){
|
||||
|
||||
if(formModel.sort){
|
||||
SetStockSort(formModel.sort,code).then(result => {
|
||||
//message.success(result)
|
||||
})
|
||||
}
|
||||
|
||||
if(alarm||formModel.alarmPrice){
|
||||
SetAlarmChangePercent(alarm,formModel.alarmPrice,code).then(result => {
|
||||
//message.success(result)
|
||||
})
|
||||
}
|
||||
SetCostPriceAndVolume(code,price,volume).then(result => {
|
||||
modalShow.value=false
|
||||
message.success(result)
|
||||
@@ -190,95 +404,268 @@ function updateCostPriceAndVolumeNew(code,price,volume){
|
||||
}
|
||||
}
|
||||
monitor()
|
||||
message.destroyAll
|
||||
message.destroyAll()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function fullscreen(){
|
||||
if(data.fullscreen){
|
||||
WindowUnfullscreen()
|
||||
}else{
|
||||
WindowFullscreen()
|
||||
}
|
||||
data.fullscreen=!data.fullscreen
|
||||
}
|
||||
|
||||
|
||||
//type 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
|
||||
function SendMessage(result,type){
|
||||
let typeName=getTypeName(type)
|
||||
let img='http://image.sinajs.cn/newchart/min/n/'+result["股票代码"]+'.gif'+"?t="+Date.now()
|
||||
let markdown="### go-stock ["+typeName+"]\n\n"+
|
||||
"### "+result["股票名称"]+"("+result["股票代码"]+")\n" +
|
||||
"- 当前价格: "+result["当前价格"]+" "+result.changePercent+"%\n" +
|
||||
"- 最高价: "+result["今日最高价"]+" "+result.highRate+"\n" +
|
||||
"- 最低价: "+result["今日最低价"]+" "+result.lowRate+"\n" +
|
||||
"- 昨收价: "+result["昨日收盘价"]+"\n" +
|
||||
"- 今开价: "+result["今日开盘价"]+"\n" +
|
||||
"- 成本价: "+result.costPrice+" "+result.profit+"% "+result.profitAmount+" ¥\n" +
|
||||
"- 成本数量: "+result.costVolume+"股\n" +
|
||||
"- 日期: "+result["日期"]+" "+result["时间"]+"\n\n"+
|
||||
"\n"
|
||||
let title=result["股票名称"]+"("+result["股票代码"]+") "+result["当前价格"]+" "+result.changePercent
|
||||
|
||||
let msg='{' +
|
||||
' "msgtype": "markdown",' +
|
||||
' "markdown": {' +
|
||||
' "title":"['+typeName+"]"+title+'",' +
|
||||
' "text": "'+markdown+'"' +
|
||||
' },' +
|
||||
' "at": {' +
|
||||
' "isAtAll": true' +
|
||||
' }' +
|
||||
' }'
|
||||
// SendDingDingMessage(msg,result["股票代码"])
|
||||
SendDingDingMessageByType(msg,result["股票代码"],type)
|
||||
}
|
||||
function aiReCheckStock(stock,stockCode) {
|
||||
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 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)
|
||||
{
|
||||
case 1:
|
||||
return "涨跌报警"
|
||||
case 2:
|
||||
return "股价报警"
|
||||
case 3:
|
||||
return "成本价报警"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
//获取高度
|
||||
function getHeight() {
|
||||
return document.documentElement.clientHeight
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-grid :x-gap="8" :cols="3" :y-gap="8">
|
||||
<n-gi v-for="result in results" >
|
||||
<n-card size="medium" style="min-height: 270px" :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :content-style="'font-size: 18px;'" closable @close="removeMonitor(result['股票代码'],result['股票名称'])">
|
||||
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
|
||||
<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" >{{result["当前价格"]}}</n-text><n-text style="padding-left: 10px;" :type="result.type">{{ result.s}}</n-text>
|
||||
<n-text size="small" v-if="result.profitAmountToday>0" :type="result.type">{{result.profitAmountToday}}</n-text>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-grid :cols="2" :y-gap="4" :x-gap="4" :item-style="'font-size: 14px;'">
|
||||
<n-gi>
|
||||
<n-text :type="'info'">{{"最高 "+result["今日最高价"]+" "+result.highRate }}</n-text>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-text :type="'info'">{{"最低 "+result["今日最低价"]+" "+result.lowRate }}</n-text>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-text :type="'info'">{{"昨收 "+result["昨日收盘价"]}}</n-text>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-text :type="'info'">{{"今开 "+result["今日开盘价"]}}</n-text>
|
||||
<n-text :type="result.type" >
|
||||
<n-number-animation :duration="1000" :precision="2" :from="result['上次当前价格']" :to="Number(result['当前价格'])" />
|
||||
</n-text>
|
||||
<n-text style="padding-left: 10px;" :type="result.type">
|
||||
<n-number-animation :duration="1000" :precision="3" :from="0" :to="result.changePercent" />%
|
||||
</n-text>
|
||||
<n-text size="small" v-if="result.costVolume>0" :type="result.type">
|
||||
<n-number-animation :duration="1000" :precision="2" :from="0" :to="result.profitAmountToday" />
|
||||
</n-text>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-grid :cols="2" :y-gap="4" :x-gap="4" >
|
||||
<n-gi>
|
||||
<n-text :type="'info'">{{"最高 "+result["今日最高价"]+" "+result.highRate }}%</n-text>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-text :type="'info'">{{"最低 "+result["今日最低价"]+" "+result.lowRate }}%</n-text>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-text :type="'info'">{{"昨收 "+result["昨日收盘价"]}}</n-text>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-text :type="'info'">{{"今开 "+result["今日开盘价"]}}</n-text>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<template #header-extra>
|
||||
<n-tag size="small" v-if="result.volume>0" :type="result.profitType">{{result.volume+"股"}}</n-tag>
|
||||
<n-button size="tiny" secondary type="primary" @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
|
||||
取消关注
|
||||
</n-button>
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||||
AI分析
|
||||
</n-button>
|
||||
|
||||
</template>
|
||||
<template #footer>
|
||||
<n-tag size="small" v-if="result.costPrice>0" :type="result.profitType">{{"成本:"+result.costPrice+" "+result.profit+"%"+" ( "+result.profitAmount+" ¥ )"}}</n-tag>
|
||||
<n-flex justify="center">
|
||||
<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+" ¥ )"}}</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #action>
|
||||
<n-flex justify="space-between">
|
||||
<n-button size="tiny" type="info" @click="setStock(result['股票代码'],result['股票名称'])"> 设置 </n-button>
|
||||
<n-button size="tiny" type="warning" @click="search(result['股票代码'],result['股票名称'])"> 详情 </n-button>
|
||||
<n-text :type="'info'">{{result["日期"]+" "+result["时间"]}}</n-text>
|
||||
<n-button size="tiny" type="info" @click="setStock(result['股票代码'],result['股票名称'])"> 成本 </n-button>
|
||||
<n-button size="tiny" type="success" @click="showFenshi(result['股票代码'],result['股票名称'])"> 分时 </n-button>
|
||||
<n-button size="tiny" type="error" @click="showK(result['股票代码'],result['股票名称'])"> 日K </n-button>
|
||||
<n-button size="tiny" type="warning" @click="search(result['股票代码'],result['股票名称'])"> 详情 </n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-card >
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-auto-complete v-model:value="data.name" type="text"
|
||||
:input-props="{
|
||||
autocomplete: 'disabled',
|
||||
}"
|
||||
:options="options"
|
||||
placeholder="股票名称或者代码"
|
||||
clearable class="input" @input="getStockList" :on-select="onSelect"/>
|
||||
<n-button type="info" @click="AddStock"> 添加 </n-button>
|
||||
|
||||
<div style="position: fixed;bottom: 18px;right:0;z-index: 10;width: 350px">
|
||||
<!-- <n-card :bordered="false">-->
|
||||
<n-input-group>
|
||||
<!-- <n-button type="error" @click="addBTN=!addBTN" > <n-icon :component="Search"/> <n-text v-if="addBTN">隐藏</n-text></n-button>-->
|
||||
<n-auto-complete v-model:value="data.name" v-if="addBTN"
|
||||
:input-props="{
|
||||
autocomplete: 'disabled',
|
||||
}"
|
||||
:options="options"
|
||||
placeholder="请输入股票/指数名称或者代码"
|
||||
clearable @update-value="getStockList" :on-select="onSelect"/>
|
||||
<n-button type="primary" @click="AddStock" v-if="addBTN">
|
||||
<n-icon :component="Add"/> 关注该股票
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<!-- </n-card>-->
|
||||
</div>
|
||||
<n-modal transform-origin="center" size="small" v-model:show="modalShow" :title="formModel.name" style="width: 400px" :preset="'card'">
|
||||
<n-form :model="formModel" :rules="{ costPrice: { required: true, message: '请输入成本'}, volume: { required: true, message: '请输入数量'} }" label-placement="left" label-width="80px">
|
||||
<n-form-item label="成本(元)" path="costPrice">
|
||||
<n-input-number v-model:value="formModel.costPrice" min="0" placeholder="请输入股票成本" />
|
||||
<n-form :model="formModel" :rules="{
|
||||
costPrice: { required: true, message: '请输入成本'},
|
||||
volume: { required: true, message: '请输入数量'},
|
||||
alarm:{required: true, message: '涨跌报警值'} ,
|
||||
alarmPrice: { required: true, message: '请输入报警价格'},
|
||||
sort: { required: true, message: '请输入排序值'},
|
||||
}" label-placement="left" label-width="80px">
|
||||
<n-form-item label="股票成本" path="costPrice">
|
||||
<n-input-number v-model:value="formModel.costPrice" min="0" placeholder="请输入股票成本" >
|
||||
<template #suffix>
|
||||
¥
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item label="数量(股)" path="volume">
|
||||
<n-input-number v-model:value="formModel.volume" min="0" placeholder="请输入股票数量" />
|
||||
<n-form-item label="股票数量" path="volume">
|
||||
<n-input-number v-model:value="formModel.volume" min="0" step="100" placeholder="请输入股票数量" >
|
||||
<template #suffix>
|
||||
股
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item label="涨跌提醒" path="alarm">
|
||||
<n-input-number v-model:value="formModel.alarm" min="0" placeholder="请输入涨跌报警值(%)" >
|
||||
<template #suffix>
|
||||
%
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item label="股价提醒" path="alarmPrice">
|
||||
<n-input-number v-model:value="formModel.alarmPrice" min="0" placeholder="请输入股价报警值(¥)" >
|
||||
<template #suffix>
|
||||
¥
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item label="股票排序" path="sort">
|
||||
<n-input-number v-model:value="formModel.sort" min="0" placeholder="请输入股价排序值" >
|
||||
</n-input-number>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume)">保存</n-button>
|
||||
<n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume,formModel.alarm,formModel)">保存</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="modalShow2" :title="data.name" style="width: 600px" :preset="'card'">
|
||||
<n-image :src="data.fenshiURL" />
|
||||
</n-modal>
|
||||
<n-modal v-model:show="modalShow3" :title="data.name" style="width: 600px" :preset="'card'">
|
||||
<n-image :src="data.kURL" />
|
||||
</n-modal>
|
||||
|
||||
<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;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>
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
.input-box {
|
||||
text-align: center;
|
||||
}
|
||||
.input {
|
||||
width: 200px;
|
||||
margin-right: 10px;
|
||||
.md-editor-preview h3{
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.light-green {
|
||||
height: 108px;
|
||||
background-color: rgba(0, 128, 0, 0.12);
|
||||
}
|
||||
.green {
|
||||
height: 108px;
|
||||
background-color: rgba(0, 128, 0, 0.24);
|
||||
}
|
||||
.md-editor-preview p{
|
||||
text-align: left !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {createApp} from 'vue'
|
||||
import naive from 'naive-ui'
|
||||
import App from './App.vue'
|
||||
import router from './router/router'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
|
||||
app.mount('#app')
|
||||
18
frontend/src/router/router.js
Normal file
@@ -0,0 +1,18 @@
|
||||
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({
|
||||
history: createMemoryHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
23
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -1,15 +1,38 @@
|
||||
// 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>;
|
||||
|
||||
export function SetAlarmChangePercent(arg1:number,arg2:number,arg3:string):Promise<string>;
|
||||
|
||||
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
|
||||
|
||||
export function SetStockSort(arg1:number,arg2:string):Promise<void>;
|
||||
|
||||
export function UnFollow(arg1:string):Promise<string>;
|
||||
|
||||
export function UpdateConfig(arg1:data.Settings):Promise<string>;
|
||||
|
||||
@@ -6,6 +6,14 @@ 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']();
|
||||
}
|
||||
|
||||
export function GetFollowList() {
|
||||
return window['go']['main']['App']['GetFollowList']();
|
||||
}
|
||||
@@ -14,14 +22,50 @@ 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);
|
||||
}
|
||||
|
||||
export function SendDingDingMessageByType(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SendDingDingMessageByType'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SetAlarmChangePercent(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SetAlarmChangePercent'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SetCostPriceAndVolume(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SetStockSort(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetStockSort'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function UnFollow(arg1) {
|
||||
return window['go']['main']['App']['UnFollow'](arg1);
|
||||
}
|
||||
|
||||
export function UpdateConfig(arg1) {
|
||||
return window['go']['main']['App']['UpdateConfig'](arg1);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ export namespace data {
|
||||
Price: number;
|
||||
PriceChange: number;
|
||||
ChangePercent: number;
|
||||
AlarmChangePercent: number;
|
||||
AlarmPrice: number;
|
||||
// Go type: time
|
||||
Time: any;
|
||||
Sort: number;
|
||||
IsDel: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedStock(source);
|
||||
@@ -25,8 +28,80 @@ export namespace data {
|
||||
this.Price = source["Price"];
|
||||
this.PriceChange = source["PriceChange"];
|
||||
this.ChangePercent = source["ChangePercent"];
|
||||
this.AlarmChangePercent = source["AlarmChangePercent"];
|
||||
this.AlarmPrice = source["AlarmPrice"];
|
||||
this.Time = this.convertValues(source["Time"], null);
|
||||
this.Sort = source["Sort"];
|
||||
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 Settings {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
tushareToken: string;
|
||||
localPushEnable: boolean;
|
||||
dingPushEnable: boolean;
|
||||
dingRobot: string;
|
||||
updateBasicInfoOnStart: boolean;
|
||||
refreshInterval: number;
|
||||
openAiEnable: boolean;
|
||||
openAiBaseUrl: string;
|
||||
openAiApiKey: string;
|
||||
openAiModelName: string;
|
||||
openAiMaxTokens: number;
|
||||
openAiTemperature: number;
|
||||
openAiApiTimeOut: number;
|
||||
prompt: string;
|
||||
checkUpdate: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Settings(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.tushareToken = source["tushareToken"];
|
||||
this.localPushEnable = source["localPushEnable"];
|
||||
this.dingPushEnable = source["dingPushEnable"];
|
||||
this.dingRobot = source["dingRobot"];
|
||||
this.updateBasicInfoOnStart = source["updateBasicInfoOnStart"];
|
||||
this.refreshInterval = source["refreshInterval"];
|
||||
this.openAiEnable = source["openAiEnable"];
|
||||
this.openAiBaseUrl = source["openAiBaseUrl"];
|
||||
this.openAiApiKey = source["openAiApiKey"];
|
||||
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 {
|
||||
@@ -132,13 +207,14 @@ export namespace data {
|
||||
"时间": string;
|
||||
"股票代码": string;
|
||||
"股票名称": string;
|
||||
"上次当前价格": number;
|
||||
"当前价格": string;
|
||||
"成交的股票数": string;
|
||||
"成交金额": string;
|
||||
"今日开盘价": string;
|
||||
"昨日收盘价": string;
|
||||
"今日最低价": string;
|
||||
"今日最高价": string;
|
||||
"今日最低价": string;
|
||||
"竞买价": string;
|
||||
"竞卖价": string;
|
||||
"买一报价": string;
|
||||
@@ -161,6 +237,18 @@ export namespace data {
|
||||
"卖四申报": string;
|
||||
"卖五报价": string;
|
||||
"卖五申报": string;
|
||||
changePercent: number;
|
||||
changePrice: number;
|
||||
highRate: number;
|
||||
lowRate: number;
|
||||
costPrice: number;
|
||||
costVolume: number;
|
||||
profit: number;
|
||||
profitAmount: number;
|
||||
profitAmountToday: number;
|
||||
sort: number;
|
||||
alarmChangePercent: number;
|
||||
alarmPrice: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StockInfo(source);
|
||||
@@ -176,13 +264,14 @@ export namespace data {
|
||||
this["时间"] = source["时间"];
|
||||
this["股票代码"] = source["股票代码"];
|
||||
this["股票名称"] = source["股票名称"];
|
||||
this["上次当前价格"] = source["上次当前价格"];
|
||||
this["当前价格"] = source["当前价格"];
|
||||
this["成交的股票数"] = source["成交的股票数"];
|
||||
this["成交金额"] = source["成交金额"];
|
||||
this["今日开盘价"] = source["今日开盘价"];
|
||||
this["昨日收盘价"] = source["昨日收盘价"];
|
||||
this["今日最低价"] = source["今日最低价"];
|
||||
this["今日最高价"] = source["今日最高价"];
|
||||
this["今日最低价"] = source["今日最低价"];
|
||||
this["竞买价"] = source["竞买价"];
|
||||
this["竞卖价"] = source["竞卖价"];
|
||||
this["买一报价"] = source["买一报价"];
|
||||
@@ -205,6 +294,119 @@ export namespace data {
|
||||
this["卖四申报"] = source["卖四申报"];
|
||||
this["卖五报价"] = source["卖五报价"];
|
||||
this["卖五申报"] = source["卖五申报"];
|
||||
this.changePercent = source["changePercent"];
|
||||
this.changePrice = source["changePrice"];
|
||||
this.highRate = source["highRate"];
|
||||
this.lowRate = source["lowRate"];
|
||||
this.costPrice = source["costPrice"];
|
||||
this.costVolume = source["costVolume"];
|
||||
this.profit = source["profit"];
|
||||
this.profitAmount = source["profitAmount"];
|
||||
this.profitAmountToday = source["profitAmountToday"];
|
||||
this.sort = source["sort"];
|
||||
this.alarmChangePercent = source["alarmChangePercent"];
|
||||
this.alarmPrice = source["alarmPrice"];
|
||||
}
|
||||
|
||||
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 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 {
|
||||
|
||||
34
go.mod
@@ -1,39 +1,63 @@
|
||||
module go-stock
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/coocood/freecache v1.2.4
|
||||
github.com/duke-git/lancet/v2 v2.3.4
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/wailsapp/wails/v2 v2.9.2
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/text v0.21.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/gorm v1.25.12
|
||||
gorm.io/plugin/dbresolver v1.5.3
|
||||
gorm.io/plugin/soft_delete v1.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.10.2 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
|
||||
github.com/leaanthony/gosod v1.0.3 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
@@ -45,10 +69,10 @@ require (
|
||||
github.com/wailsapp/go-webview2 v1.0.16 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
|
||||
114
go.sum
@@ -1,5 +1,19 @@
|
||||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb h1:noKVm2SsG4v0Yd0lHNtFYc9EUxIVvrr4kJ6hM8wvIYU=
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
|
||||
github.com/chromedp/chromedp v0.11.2 h1:ZRHTh7DjbNTlfIv3NFTbB7eVeu5XCNkgrpcGSpn2oX0=
|
||||
github.com/chromedp/chromedp v0.11.2/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
||||
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -7,6 +21,20 @@ github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvY
|
||||
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
@@ -17,8 +45,19 @@ github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CA
|
||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
@@ -27,8 +66,12 @@ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4P
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
|
||||
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
@@ -44,6 +87,12 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
|
||||
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
@@ -53,6 +102,15 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -67,7 +125,9 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@@ -93,25 +153,40 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -122,28 +197,45 @@ golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
@@ -153,11 +245,17 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
|
||||
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
|
||||
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
|
||||
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
|
||||
gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU=
|
||||
gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
|
||||
111
main.go
@@ -6,13 +6,19 @@ import (
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/logger"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu/keys"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/mac"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/models"
|
||||
"log"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
@@ -21,50 +27,95 @@ var assets embed.FS
|
||||
//go:embed build/appicon.png
|
||||
var icon []byte
|
||||
|
||||
//go:embed build/app.ico
|
||||
var icon2 []byte
|
||||
|
||||
//go:embed build/stock_basic.json
|
||||
var stocksBin []byte
|
||||
|
||||
//go:generate cp -R ./data ./build/bin
|
||||
|
||||
var Version string
|
||||
var VersionCommit string
|
||||
|
||||
func main() {
|
||||
checkDir("data")
|
||||
db.Init("")
|
||||
db.Dao.AutoMigrate(&data.StockInfo{})
|
||||
db.Dao.AutoMigrate(&data.StockBasic{})
|
||||
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 {
|
||||
initStockData()
|
||||
go initStockData()
|
||||
}
|
||||
|
||||
data.NewStockDataApi().GetStockBaseInfo()
|
||||
updateBasicInfo()
|
||||
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
AppMenu := menu.NewMenu()
|
||||
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)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
//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",
|
||||
Width: 1366,
|
||||
Height: 860,
|
||||
MinWidth: 1024,
|
||||
MinHeight: 768,
|
||||
MaxWidth: 1280,
|
||||
MaxHeight: 960,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: false,
|
||||
StartHidden: false,
|
||||
HideWindowOnClose: false,
|
||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
Assets: assets,
|
||||
Menu: nil,
|
||||
Logger: nil,
|
||||
LogLevel: logger.DEBUG,
|
||||
OnStartup: app.startup,
|
||||
OnDomReady: app.domReady,
|
||||
OnBeforeClose: app.beforeClose,
|
||||
OnShutdown: app.shutdown,
|
||||
WindowStartState: options.Normal,
|
||||
Title: "go-stock",
|
||||
Width: 1366,
|
||||
Height: 920,
|
||||
MinWidth: 1024,
|
||||
MinHeight: 768,
|
||||
MaxWidth: 1920,
|
||||
MaxHeight: 960,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: true,
|
||||
StartHidden: false,
|
||||
HideWindowOnClose: false,
|
||||
EnableDefaultContextMenu: true,
|
||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
Assets: assets,
|
||||
Menu: AppMenu,
|
||||
Logger: nil,
|
||||
LogLevel: logger.DEBUG,
|
||||
LogLevelProduction: logger.ERROR,
|
||||
OnStartup: app.startup,
|
||||
OnDomReady: app.domReady,
|
||||
OnBeforeClose: app.beforeClose,
|
||||
OnShutdown: app.shutdown,
|
||||
WindowStartState: options.Normal,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
@@ -100,6 +151,16 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func updateBasicInfo() {
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
if config.UpdateBasicInfoOnStart {
|
||||
//更新基本信息
|
||||
go data.NewStockDataApi().GetStockBaseInfo()
|
||||
go data.NewStockDataApi().GetIndexBasic()
|
||||
}
|
||||
}
|
||||
|
||||
func initStockData() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"request_id":"359a096c-0a5a-4b07-818f-3241738d5e73","code":40203,"data":null,"msg":"抱歉,您每小时最多访问该接口1次,权限的具体详情访问:https://tushare.pro/document/1?doc_id=108。"}
|
||||
@@ -7,13 +7,13 @@
|
||||
"frontend:dev:serverUrl": "http://localhost:5173",
|
||||
"author": {
|
||||
"name": "spark",
|
||||
"email": "wzl@huazx.cn"
|
||||
"email": "sparkmemory@163.com"
|
||||
},
|
||||
"info": {
|
||||
"companyName": "sparkmemory",
|
||||
"productName": "go-stock",
|
||||
"productVersion": "1.0.0",
|
||||
"copyright": "Copyright#sparkmemory@163.com",
|
||||
"comments": "股票行情实时获取"
|
||||
"comments": "股票行情实时获取,AI赋能分析股票"
|
||||
}
|
||||
}
|
||||
|
||||