mirror of
https://github.com/ArvinLovegood/go-stock.git
synced 2025-07-19 00:00:09 +08:00
feat(fund):添加基金监控和查询功能
- 新增基金数据 API,实现基金信息爬取和数据库操作 - 添加基金监控逻辑,定期更新基金净值信息 - 实现基金列表查询、关注和取消关注功能 - 新增基金相关前端组件,展示基金信息和操作
This commit is contained in:
parent
1b31ff04df
commit
9a6e210bae
37
app.go
37
app.go
@ -106,7 +106,15 @@ func (a *App) domReady(ctx context.Context) {
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
MonitorStockPrices(a)
|
||||
}
|
||||
}()
|
||||
|
||||
//刷新基金净值信息
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
MonitorFundPrices(a)
|
||||
}
|
||||
}()
|
||||
|
||||
@ -123,7 +131,8 @@ func (a *App) domReady(ctx context.Context) {
|
||||
}()
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
||||
go MonitorStockPrices(a)
|
||||
|
||||
go MonitorFundPrices(a)
|
||||
go data.NewFundApi().AllFund()
|
||||
//检查新版本
|
||||
go func() {
|
||||
a.CheckUpdate()
|
||||
@ -267,6 +276,19 @@ func IsUSTradingTime(date time.Time) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
func MonitorFundPrices(a *App) {
|
||||
dest := &[]data.FollowedFund{}
|
||||
db.Dao.Model(&data.FollowedFund{}).Find(dest)
|
||||
for _, follow := range *dest {
|
||||
_, err := data.NewFundApi().CrawlFundBasic(follow.Code)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("获取基金基本信息失败,基金代码:%s,错误信息:%s", follow.Code, err.Error())
|
||||
continue
|
||||
}
|
||||
data.NewFundApi().CrawlFundNetEstimatedUnit(follow.Code)
|
||||
data.NewFundApi().CrawlFundNetUnitValue(follow.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func MonitorStockPrices(a *App) {
|
||||
dest := &[]data.FollowedStock{}
|
||||
@ -731,3 +753,16 @@ func (a *App) ShareAnalysis(stockCode, stockName string) string {
|
||||
}
|
||||
return "获取分析结果失败"
|
||||
}
|
||||
|
||||
func (a *App) GetfundList(key string) []data.FundBasic {
|
||||
return data.NewFundApi().GetFundList(key)
|
||||
}
|
||||
func (a *App) GetFollowedFund() []data.FollowedFund {
|
||||
return data.NewFundApi().GetFollowedFund()
|
||||
}
|
||||
func (a *App) FollowFund(fundCode string) string {
|
||||
return data.NewFundApi().FollowFund(fundCode)
|
||||
}
|
||||
func (a *App) UnFollowFund(fundCode string) string {
|
||||
return data.NewFundApi().UnFollowFund(fundCode)
|
||||
}
|
||||
|
379
backend/data/fund_data_api.go
Normal file
379
backend/data/fund_data_api.go
Normal file
@ -0,0 +1,379 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FundApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
}
|
||||
|
||||
func NewFundApi() *FundApi {
|
||||
return &FundApi{
|
||||
client: resty.New(),
|
||||
config: getConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
type FollowedFund struct {
|
||||
gorm.Model
|
||||
Code string `json:"code" gorm:"index"` // 基金代码
|
||||
Name string `json:"name"` // 基金简称
|
||||
|
||||
NetUnitValue *float64 `json:"netUnitValue"` // 单位净值
|
||||
NetUnitValueDate string `json:"netUnitValueDate"` // 单位净值日期
|
||||
NetEstimatedUnit *float64 `json:"netEstimatedUnit"` // 估算单位净值
|
||||
NetEstimatedTime string `json:"netEstimatedUnitTime"` // 估算单位净值日期
|
||||
NetAccumulated *float64 `json:"netAccumulated"` // 累计净值
|
||||
FundBasic FundBasic `json:"fundBasic" gorm:"foreignKey:Code;references:Code"`
|
||||
}
|
||||
|
||||
func (FollowedFund) TableName() string {
|
||||
return "followed_fund"
|
||||
}
|
||||
|
||||
// FundBasic 基金基本信息结构体
|
||||
type FundBasic struct {
|
||||
gorm.Model
|
||||
Code string `json:"code" gorm:"index"` // 基金代码
|
||||
Name string `json:"name"` // 基金简称
|
||||
FullName string `json:"fullName"` // 基金全称
|
||||
Type string `json:"type"` // 基金类型
|
||||
Establishment string `json:"establishment"` // 成立日期
|
||||
Scale string `json:"scale"` // 最新规模(亿元)
|
||||
Company string `json:"company"` // 基金管理人
|
||||
Manager string `json:"manager"` // 基金经理
|
||||
Rating string `json:"rating"` //基金评级
|
||||
TrackingTarget string `json:"trackingTarget"` //跟踪标的
|
||||
|
||||
NetUnitValue *float64 `json:"netUnitValue"` // 单位净值
|
||||
NetUnitValueDate string `json:"netUnitValueDate"` // 单位净值日期
|
||||
NetEstimatedUnit *float64 `json:"netEstimatedUnit"` // 估算单位净值
|
||||
NetEstimatedTime string `json:"netEstimatedUnitTime"` // 估算单位净值日期
|
||||
NetAccumulated *float64 `json:"netAccumulated"` // 累计净值
|
||||
|
||||
//净值涨跌幅: 近1月,近3月,近6月,近1年,近3年,近5年,今年来,成立来
|
||||
NetGrowth1 *float64 `json:"netGrowth1"` //近1月
|
||||
NetGrowth3 *float64 `json:"netGrowth3"` //近3月
|
||||
NetGrowth6 *float64 `json:"netGrowth6"` //近6月
|
||||
NetGrowth12 *float64 `json:"netGrowth12"` //近1年
|
||||
NetGrowth36 *float64 `json:"netGrowth36"` //近3年
|
||||
NetGrowth60 *float64 `json:"netGrowth60"` //近5年
|
||||
NetGrowthYTD *float64 `json:"netGrowthYTD"` //今年来
|
||||
NetGrowthAll *float64 `json:"netGrowthAll"` //成立来
|
||||
}
|
||||
|
||||
func (FundBasic) TableName() string {
|
||||
return "fund_basic"
|
||||
}
|
||||
|
||||
// CrawlFundBasic 爬取基金基本信息
|
||||
func (f *FundApi) CrawlFundBasic(fundCode string) (*FundBasic, error) {
|
||||
crawler := CrawlerApi{
|
||||
crawlerBaseInfo: CrawlerBaseInfo{
|
||||
Name: "天天基金",
|
||||
BaseUrl: "http://fund.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(f.config.CrawlTimeOut)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
crawler = crawler.NewCrawler(ctx, crawler.crawlerBaseInfo)
|
||||
url := fmt.Sprintf("%s/%s.html", crawler.crawlerBaseInfo.BaseUrl, fundCode)
|
||||
logger.SugaredLogger.Infof("CrawlFundBasic url:%s", url)
|
||||
|
||||
// 使用现有爬虫框架解析页面
|
||||
htmlContent, ok := crawler.GetHtml(url, ".merchandiseDetail", true)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("页面解析失败")
|
||||
}
|
||||
|
||||
fund := &FundBasic{Code: fundCode}
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析基础信息
|
||||
name := doc.Find(".merchandiseDetail .fundDetail-tit").First().Text()
|
||||
fund.Name = strings.TrimSpace(strutil.ReplaceWithMap(name, map[string]string{"查看相关ETF>": ""}))
|
||||
logger.SugaredLogger.Infof("基金名称:%s", fund.Name)
|
||||
|
||||
doc.Find(".infoOfFund table td ").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(s.Text(), true)
|
||||
logger.SugaredLogger.Infof("基金信息:%+v", text)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
splitEx := strutil.SplitEx(text, ":", true)
|
||||
if strutil.ContainsAny(text, []string{"基金类型", "类型"}) {
|
||||
fund.Type = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"成立日期", "成立日"}) {
|
||||
fund.Establishment = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"基金规模", "规模"}) {
|
||||
fund.Scale = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"管理人", "基金公司"}) {
|
||||
fund.Company = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"基金经理", "经理人"}) {
|
||||
fund.Manager = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"基金评级", "评级"}) {
|
||||
fund.Rating = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"跟踪标的", "标的"}) {
|
||||
fund.TrackingTarget = splitEx[1]
|
||||
}
|
||||
})
|
||||
|
||||
//获取基金净值涨跌幅信息
|
||||
doc.Find(".dataOfFund dl > dd").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(s.Text(), true)
|
||||
logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", text)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
splitEx := strutil.SplitAndTrim(text, ":", "%")
|
||||
toFloat, err1 := convertor.ToFloat(splitEx[1])
|
||||
if err1 != nil {
|
||||
logger.SugaredLogger.Errorf("转换失败:%+v", err)
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", toFloat)
|
||||
if strutil.ContainsAny(text, []string{"近1月"}) {
|
||||
fund.NetGrowth1 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近3月"}) {
|
||||
fund.NetGrowth3 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近6月"}) {
|
||||
fund.NetGrowth6 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近1年"}) {
|
||||
fund.NetGrowth12 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近3年"}) {
|
||||
fund.NetGrowth36 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近5年"}) {
|
||||
fund.NetGrowth60 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"今年来"}) {
|
||||
fund.NetGrowthYTD = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"成立来"}) {
|
||||
fund.NetGrowthAll = &toFloat
|
||||
}
|
||||
})
|
||||
doc.Find(".dataOfFund dl > dd.dataNums,.dataOfFund dl > dt").Each(func(i int, s *goquery.Selection) {
|
||||
text := s.Text()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
logger.SugaredLogger.Infof("净值信息:%+v", text)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof("基金信息:%+v", fund)
|
||||
|
||||
count := int64(0)
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Count(&count)
|
||||
if count == 0 {
|
||||
db.Dao.Create(fund)
|
||||
} else {
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
|
||||
return fund, nil
|
||||
}
|
||||
|
||||
func (f *FundApi) GetFundList(key string) []FundBasic {
|
||||
var funds []FundBasic
|
||||
db.Dao.Where("code like ? or name like ?", "%"+key+"%", "%"+key+"%").Limit(10).Find(&funds)
|
||||
return funds
|
||||
}
|
||||
|
||||
func (f *FundApi) GetFollowedFund() []FollowedFund {
|
||||
var funds []FollowedFund
|
||||
db.Dao.Preload("FundBasic").Find(&funds)
|
||||
return funds
|
||||
}
|
||||
func (f *FundApi) FollowFund(fundCode string) string {
|
||||
var fund FundBasic
|
||||
db.Dao.Where("code=?", fundCode).First(&fund)
|
||||
if fund.Code != "" {
|
||||
follow := &FollowedFund{
|
||||
Code: fundCode,
|
||||
Name: fund.Name,
|
||||
}
|
||||
err := db.Dao.Model(follow).Where("code = ?", fundCode).FirstOrCreate(follow, "code", fund.Code).Error
|
||||
if err != nil {
|
||||
return "关注失败"
|
||||
}
|
||||
return "关注成功"
|
||||
} else {
|
||||
return "基金信息不存在"
|
||||
}
|
||||
}
|
||||
func (f *FundApi) UnFollowFund(fundCode string) string {
|
||||
var fund FollowedFund
|
||||
db.Dao.Where("code=?", fundCode).First(&fund)
|
||||
if fund.Code != "" {
|
||||
err := db.Dao.Model(&fund).Delete(&fund).Error
|
||||
if err != nil {
|
||||
return "取消关注失败"
|
||||
}
|
||||
return "取消关注成功"
|
||||
} else {
|
||||
return "基金信息不存在"
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FundApi) AllFund() {
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36").
|
||||
Get("https://fund.eastmoney.com/allfund.html")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//中文编码
|
||||
htmlContent := GB18030ToUTF8(response.Body())
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
cnt := 0
|
||||
doc.Find("ul.num_right li").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.SplitEx(s.Text(), "|", true)
|
||||
if len(text) > 0 {
|
||||
cnt++
|
||||
name := text[0]
|
||||
str := strutil.SplitAndTrim(name, ")", "(", ")")
|
||||
logger.SugaredLogger.Infof("%d,基金信息 code:%s,name:%s", cnt, str[0], str[1])
|
||||
//f.CrawlFundBasic(str[0])
|
||||
|
||||
fund := &FundBasic{
|
||||
Code: str[0],
|
||||
Name: str[1],
|
||||
}
|
||||
count := int64(0)
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Count(&count)
|
||||
if count == 0 {
|
||||
db.Dao.Create(fund)
|
||||
} else {
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type FundNetUnitValue struct {
|
||||
Fundcode string `json:"fundcode"`
|
||||
Name string `json:"name"`
|
||||
Jzrq string `json:"jzrq"`
|
||||
Dwjz string `json:"dwjz"`
|
||||
Gsz string `json:"gsz"`
|
||||
Gszzl string `json:"gszzl"`
|
||||
Gztime string `json:"gztime"`
|
||||
}
|
||||
|
||||
// CrawlFundNetEstimatedUnit 爬取净值估算值
|
||||
func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
|
||||
var fundNetUnitValue FundNetUnitValue
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36").
|
||||
SetHeader("Referer", "https://fund.eastmoney.com/").
|
||||
SetQueryParams(map[string]string{"rt": strconv.FormatInt(time.Now().UnixMilli(), 10)}).
|
||||
Get(fmt.Sprintf("https://fundgz.1234567.com.cn/js/%s.js", code))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
if response.StatusCode() == 200 {
|
||||
htmlContent := string(response.Body())
|
||||
logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
|
||||
if strings.Contains(htmlContent, "jsonpgz") {
|
||||
htmlContent = strutil.Trim(htmlContent, "jsonpgz(", ");")
|
||||
htmlContent = strutil.Trim(htmlContent, ");")
|
||||
logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent)
|
||||
err := json.Unmarshal([]byte(htmlContent), &fundNetUnitValue)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
fund := &FollowedFund{
|
||||
Code: fundNetUnitValue.Fundcode,
|
||||
Name: fundNetUnitValue.Name,
|
||||
NetEstimatedTime: fundNetUnitValue.Gztime,
|
||||
}
|
||||
netEstimatedUnit, err := convertor.ToFloat(fundNetUnitValue.Gsz)
|
||||
if err == nil {
|
||||
fund.NetEstimatedUnit = &netEstimatedUnit
|
||||
}
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CrawlFundNetUnitValue 爬取净值
|
||||
func (f *FundApi) CrawlFundNetUnitValue(code string) {
|
||||
// var fundNetUnitValue FundNetUnitValue
|
||||
url := fmt.Sprintf("http://hq.sinajs.cn/rn=%d&list=f_%s", time.Now().UnixMilli(), code)
|
||||
logger.SugaredLogger.Infof("url:%s", url)
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "hq.sinajs.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").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
if response.StatusCode() == 200 {
|
||||
data := string(GB18030ToUTF8(response.Body()))
|
||||
logger.SugaredLogger.Infof("data:%s", data)
|
||||
datas := strutil.SplitAndTrim(data, "=", "\"")
|
||||
if len(datas) >= 2 {
|
||||
//codex := strings.Split(datas[0], "hq_str_f_")[1]
|
||||
parts := strutil.SplitAndTrim(datas[1], ",", "\"")
|
||||
logger.SugaredLogger.Infof("parts:%s", parts)
|
||||
val, err := convertor.ToFloat(parts[1])
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
fund := &FollowedFund{
|
||||
Name: parts[0],
|
||||
Code: code,
|
||||
NetUnitValue: &val,
|
||||
NetUnitValueDate: parts[4],
|
||||
}
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
23
backend/data/fund_data_api_test.go
Normal file
23
backend/data/fund_data_api_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCrawlFundBasic(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
db.Dao.AutoMigrate(&FundBasic{})
|
||||
api := NewFundApi()
|
||||
|
||||
//api.CrawlFundBasic("510630")
|
||||
//api.CrawlFundBasic("159688")
|
||||
//
|
||||
api.AllFund()
|
||||
}
|
||||
|
||||
func TestCrawlFundNetUnitValue(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
api := NewFundApi()
|
||||
api.CrawlFundNetUnitValue("016533")
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"go-stock/backend/logger"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -40,4 +42,6 @@ func TestReplaceSensitiveWords(t *testing.T) {
|
||||
txt := "新 希 望习近平"
|
||||
txt2 := ReplaceSensitiveWords(txt)
|
||||
logger.SugaredLogger.Infof("ReplaceSensitiveWords(%s)", txt2)
|
||||
|
||||
os.WriteFile("words.txt", []byte(slice.Join(SensitiveWords, "\n")), 0644)
|
||||
}
|
||||
|
@ -52,20 +52,22 @@ const menuOptions = ref([
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
NGradientText,
|
||||
RouterLink,
|
||||
{
|
||||
type: 'warning',
|
||||
style: {
|
||||
'text-decoration': 'line-through',
|
||||
to: {
|
||||
name: 'fund',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
},
|
||||
}
|
||||
},
|
||||
{ default: () => '基金自选' }
|
||||
{ default: () => '基金自选',}
|
||||
),
|
||||
key: 'fund',
|
||||
icon: renderIcon(SparklesOutline),
|
||||
children:[
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '敬请期待!'}),
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '功能完善中!'}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(AlarmOutline),
|
||||
|
207
frontend/src/components/fund.vue
Normal file
207
frontend/src/components/fund.vue
Normal file
@ -0,0 +1,207 @@
|
||||
<script setup>
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from "vue";
|
||||
import {Add, ChatboxOutline} from "@vicons/ionicons5";
|
||||
import {NButton, NEllipsis, NText, useMessage} from "naive-ui";
|
||||
import {
|
||||
FollowFund,
|
||||
GetConfig,
|
||||
GetFollowedFund,
|
||||
GetfundList,
|
||||
GetVersionInfo,
|
||||
UnFollowFund
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import vueDanmaku from 'vue3-danmaku'
|
||||
|
||||
const danmus = ref([])
|
||||
const ws = ref(null)
|
||||
const icon = ref(null)
|
||||
const message = useMessage()
|
||||
|
||||
const data = reactive({
|
||||
modelName:"",
|
||||
chatId: "",
|
||||
question:"",
|
||||
name: "",
|
||||
code: "",
|
||||
fenshiURL:"",
|
||||
kURL:"",
|
||||
fullscreen: false,
|
||||
airesult: "",
|
||||
openAiEnable: false,
|
||||
loading: true,
|
||||
enableDanmu: false,
|
||||
})
|
||||
|
||||
const followList=ref([])
|
||||
const options=ref([])
|
||||
const ticker=ref({})
|
||||
|
||||
onBeforeMount(()=>{
|
||||
GetConfig().then(result => {
|
||||
if (result.openAiEnable) {
|
||||
data.openAiEnable = true
|
||||
}
|
||||
if (result.enableDanmu) {
|
||||
data.enableDanmu = true
|
||||
}
|
||||
})
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
GetVersionInfo().then((res) => {
|
||||
icon.value = res.icon;
|
||||
});
|
||||
// 创建 WebSocket 连接
|
||||
ws.value = new WebSocket('ws://8.134.249.145:16688/ws'); // 替换为你的 WebSocket 服务器地址
|
||||
//ws.value = new WebSocket('ws://localhost:16688/ws'); // 替换为你的 WebSocket 服务器地址
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('WebSocket 连接已打开');
|
||||
};
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
if(data.enableDanmu){
|
||||
danmus.value.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
console.error('WebSocket 错误:', error);
|
||||
};
|
||||
|
||||
ws.value.onclose = () => {
|
||||
console.log('WebSocket 连接已关闭');
|
||||
};
|
||||
|
||||
ticker.value=setInterval(() => {
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
})
|
||||
}, 1000*60)
|
||||
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(ticker.value)
|
||||
ws.value.close()
|
||||
})
|
||||
|
||||
|
||||
|
||||
function SendDanmu(){
|
||||
ws.value.send(data.name)
|
||||
}
|
||||
function AddFund(){
|
||||
FollowFund(data.code).then(result=>{
|
||||
if(result){
|
||||
message.success("关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
function unFollow(code){
|
||||
UnFollowFund(code).then(result=>{
|
||||
if(result){
|
||||
message.success("取消关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getFundList(value){
|
||||
GetfundList(value).then(result=>{
|
||||
options.value=[]
|
||||
result.forEach(item=>{
|
||||
options.value.push({
|
||||
label: item.name,
|
||||
value: item.code,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
function onSelectFund(value){
|
||||
data.code=value
|
||||
}
|
||||
function formatterTitle(title){
|
||||
return () => h(NEllipsis,{
|
||||
style: {
|
||||
'font-size': '16px',
|
||||
'max-width': '180px',
|
||||
},
|
||||
},{default: () => title,}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vue-danmaku v-model:danmus="danmus" style="height:100px; width:100%;z-index: 9;position:absolute; top: 400px; pointer-events: none;" ></vue-danmaku>
|
||||
<n-flex justify="start" >
|
||||
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
|
||||
<n-gi v-for="info in followList" style="margin-left: 2px" onmouseover="this.style.border='1px solid #3498db' " onmouseout="this.style.border='0px'">
|
||||
<n-card :title="formatterTitle(info.name)">
|
||||
<template #header-extra>
|
||||
<n-tag size="small" :bordered="false" type="info">{{info.code}}</n-tag>
|
||||
<n-tag size="small" :bordered="false" type="success" @click="unFollow(info.code)"> 取消关注</n-tag>
|
||||
</template>
|
||||
<n-tag size="small" :type="info.netEstimatedUnit>0?'error':'success'" :bordered="false" v-if="info.netEstimatedUnit">估算净值:{{info.netEstimatedUnit}}({{info.netEstimatedUnitTime}})</n-tag>
|
||||
<n-divider vertical></n-divider>
|
||||
<n-tag size="small" :type="info.netUnitValue>0?'error':'success'" :bordered="false" v-if="info.netUnitValue">单位净值:{{info.netUnitValue}}({{info.netUnitValueDate}})</n-tag>
|
||||
<n-divider v-if="info.netUnitValue"></n-divider>
|
||||
<n-flex justify="start">
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth1>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth1">近一月:{{info.fundBasic.netGrowth1}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth3>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth3">近三月:{{info.fundBasic.netGrowth3}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth6>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth6">近六月:{{info.fundBasic.netGrowth6}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth12>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth12">近一年:{{info.fundBasic.netGrowth12}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth36>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth36">近三年:{{info.fundBasic.netGrowth36}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth60>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth60">近五年:{{info.fundBasic.netGrowth60}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowthYTD>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowthYTD" >今年来:{{info.fundBasic.netGrowthYTD}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowthAll>0?'error':'success'" :bordered="false" >成立来:{{info.fundBasic.netGrowthAll}}%</n-tag>
|
||||
</n-flex>
|
||||
<template #footer>
|
||||
<n-flex justify="start">
|
||||
<n-tag size="small" :bordered="false" type="warning"> {{info.fundBasic.type}}</n-tag>
|
||||
<n-tag size="small" :bordered="false" type="info"> {{info.fundBasic.company}}:{{info.fundBasic.manager}}</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
<!-- <template #action>
|
||||
<n-flex justify="end">
|
||||
<n-button size="tiny" type="warning" @click="unFollow(info.code)">取消关注</n-button>
|
||||
</n-flex>
|
||||
</template>-->
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
<div style="position: fixed;bottom: 18px;right:0;z-index: 10;width: 480px">
|
||||
<n-input-group >
|
||||
<n-auto-complete v-model:value="data.name"
|
||||
:input-props="{
|
||||
autocomplete: 'disabled',
|
||||
}"
|
||||
:options="options"
|
||||
placeholder="基金名称/代码/弹幕"
|
||||
clearable @update-value="getFundList" :on-select="onSelectFund"/>
|
||||
<n-button type="primary" @click="AddFund">
|
||||
<n-icon :component="Add"/> 关注
|
||||
</n-button>
|
||||
<n-button type="error" @click="SendDanmu" v-if="data.enableDanmu">
|
||||
<n-icon :component="ChatboxOutline"/> 发送弹幕
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -394,7 +394,7 @@ function getStockList(value){
|
||||
}
|
||||
|
||||
async function updateData(result) {
|
||||
console.log("stock_price",result['日期'],result['时间'],result['股票代码'],result['股票名称'],result['当前价格'],result['盘前盘后'])
|
||||
//console.log("stock_price",result['日期'],result['时间'],result['股票代码'],result['股票名称'],result['当前价格'],result['盘前盘后'])
|
||||
|
||||
if(result["当前价格"]<=0){
|
||||
result["当前价格"]=result["卖一报价"]
|
||||
@ -842,7 +842,7 @@ function share(code,name){
|
||||
</n-card >
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<div style="position: fixed;bottom: 18px;right:0;z-index: 10;width: 420px">
|
||||
<div style="position: fixed;bottom: 18px;right:0;z-index: 10;width: 460px">
|
||||
<!-- <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>-->
|
||||
@ -855,7 +855,7 @@ function share(code,name){
|
||||
placeholder="股票指数名称/代码/弹幕"
|
||||
clearable @update-value="getStockList" :on-select="onSelect"/>
|
||||
<n-button type="primary" @click="AddStock" v-if="addBTN">
|
||||
<n-icon :component="Add"/> 关注该股票
|
||||
<n-icon :component="Add"/> 关注
|
||||
</n-button>
|
||||
<n-button type="error" @click="SendDanmu" v-if="data.enableDanmu">
|
||||
<n-icon :component="ChatboxOutline"/> 发送弹幕
|
||||
|
@ -3,9 +3,11 @@ import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import stockView from '../components/stock.vue'
|
||||
import settingsView from '../components/settings.vue'
|
||||
import about from "../components/about.vue";
|
||||
import fundView from "../components/fund.vue";
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: stockView,name: 'stock' },
|
||||
{ path: '/fund', component: fundView,name: 'fund' },
|
||||
{ path: '/settings/:id', component: settingsView,name: 'settings' },
|
||||
{ path: '/about', component: about,name: 'about' },
|
||||
]
|
||||
|
8
frontend/wailsjs/go/main/App.d.ts
vendored
8
frontend/wailsjs/go/main/App.d.ts
vendored
@ -9,16 +9,22 @@ export function ExportConfig():Promise<string>;
|
||||
|
||||
export function Follow(arg1:string):Promise<string>;
|
||||
|
||||
export function FollowFund(arg1:string):Promise<string>;
|
||||
|
||||
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
|
||||
|
||||
export function GetConfig():Promise<data.Settings>;
|
||||
|
||||
export function GetFollowList():Promise<any>;
|
||||
|
||||
export function GetFollowedFund():Promise<Array<data.FollowedFund>>;
|
||||
|
||||
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
|
||||
|
||||
export function GetVersionInfo():Promise<models.VersionInfo>;
|
||||
|
||||
export function GetfundList(arg1:string):Promise<Array<data.FundBasic>>;
|
||||
|
||||
export function Greet(arg1:string):Promise<data.StockInfo>;
|
||||
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||
@ -39,4 +45,6 @@ export function ShareAnalysis(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function UnFollow(arg1:string):Promise<string>;
|
||||
|
||||
export function UnFollowFund(arg1:string):Promise<string>;
|
||||
|
||||
export function UpdateConfig(arg1:data.Settings):Promise<string>;
|
||||
|
@ -14,6 +14,10 @@ export function Follow(arg1) {
|
||||
return window['go']['main']['App']['Follow'](arg1);
|
||||
}
|
||||
|
||||
export function FollowFund(arg1) {
|
||||
return window['go']['main']['App']['FollowFund'](arg1);
|
||||
}
|
||||
|
||||
export function GetAIResponseResult(arg1) {
|
||||
return window['go']['main']['App']['GetAIResponseResult'](arg1);
|
||||
}
|
||||
@ -26,6 +30,10 @@ export function GetFollowList() {
|
||||
return window['go']['main']['App']['GetFollowList']();
|
||||
}
|
||||
|
||||
export function GetFollowedFund() {
|
||||
return window['go']['main']['App']['GetFollowedFund']();
|
||||
}
|
||||
|
||||
export function GetStockList(arg1) {
|
||||
return window['go']['main']['App']['GetStockList'](arg1);
|
||||
}
|
||||
@ -34,6 +42,10 @@ export function GetVersionInfo() {
|
||||
return window['go']['main']['App']['GetVersionInfo']();
|
||||
}
|
||||
|
||||
export function GetfundList(arg1) {
|
||||
return window['go']['main']['App']['GetfundList'](arg1);
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
@ -74,6 +86,10 @@ export function UnFollow(arg1) {
|
||||
return window['go']['main']['App']['UnFollow'](arg1);
|
||||
}
|
||||
|
||||
export function UnFollowFund(arg1) {
|
||||
return window['go']['main']['App']['UnFollowFund'](arg1);
|
||||
}
|
||||
|
||||
export function UpdateConfig(arg1) {
|
||||
return window['go']['main']['App']['UpdateConfig'](arg1);
|
||||
}
|
||||
|
@ -1,5 +1,146 @@
|
||||
export namespace data {
|
||||
|
||||
export class FundBasic {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
code: string;
|
||||
name: string;
|
||||
fullName: string;
|
||||
type: string;
|
||||
establishment: string;
|
||||
scale: string;
|
||||
company: string;
|
||||
manager: string;
|
||||
rating: string;
|
||||
trackingTarget: string;
|
||||
netUnitValue?: number;
|
||||
netUnitValueDate: string;
|
||||
netEstimatedUnit?: number;
|
||||
netEstimatedUnitTime: string;
|
||||
netAccumulated?: number;
|
||||
netGrowth1?: number;
|
||||
netGrowth3?: number;
|
||||
netGrowth6?: number;
|
||||
netGrowth12?: number;
|
||||
netGrowth36?: number;
|
||||
netGrowth60?: number;
|
||||
netGrowthYTD?: number;
|
||||
netGrowthAll?: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FundBasic(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.code = source["code"];
|
||||
this.name = source["name"];
|
||||
this.fullName = source["fullName"];
|
||||
this.type = source["type"];
|
||||
this.establishment = source["establishment"];
|
||||
this.scale = source["scale"];
|
||||
this.company = source["company"];
|
||||
this.manager = source["manager"];
|
||||
this.rating = source["rating"];
|
||||
this.trackingTarget = source["trackingTarget"];
|
||||
this.netUnitValue = source["netUnitValue"];
|
||||
this.netUnitValueDate = source["netUnitValueDate"];
|
||||
this.netEstimatedUnit = source["netEstimatedUnit"];
|
||||
this.netEstimatedUnitTime = source["netEstimatedUnitTime"];
|
||||
this.netAccumulated = source["netAccumulated"];
|
||||
this.netGrowth1 = source["netGrowth1"];
|
||||
this.netGrowth3 = source["netGrowth3"];
|
||||
this.netGrowth6 = source["netGrowth6"];
|
||||
this.netGrowth12 = source["netGrowth12"];
|
||||
this.netGrowth36 = source["netGrowth36"];
|
||||
this.netGrowth60 = source["netGrowth60"];
|
||||
this.netGrowthYTD = source["netGrowthYTD"];
|
||||
this.netGrowthAll = source["netGrowthAll"];
|
||||
}
|
||||
|
||||
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 FollowedFund {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
code: string;
|
||||
name: string;
|
||||
netUnitValue?: number;
|
||||
netUnitValueDate: string;
|
||||
netEstimatedUnit?: number;
|
||||
netEstimatedUnitTime: string;
|
||||
netAccumulated?: number;
|
||||
fundBasic: FundBasic;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedFund(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.code = source["code"];
|
||||
this.name = source["name"];
|
||||
this.netUnitValue = source["netUnitValue"];
|
||||
this.netUnitValueDate = source["netUnitValueDate"];
|
||||
this.netEstimatedUnit = source["netEstimatedUnit"];
|
||||
this.netEstimatedUnitTime = source["netEstimatedUnitTime"];
|
||||
this.netAccumulated = source["netAccumulated"];
|
||||
this.fundBasic = this.convertValues(source["fundBasic"], FundBasic);
|
||||
}
|
||||
|
||||
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
|
||||
|
2
main.go
2
main.go
@ -63,6 +63,8 @@ func main() {
|
||||
db.Dao.AutoMigrate(&models.AIResponseResult{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoHK{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoUS{})
|
||||
db.Dao.AutoMigrate(&data.FollowedFund{})
|
||||
db.Dao.AutoMigrate(&data.FundBasic{})
|
||||
|
||||
if stocksBin != nil && len(stocksBin) > 0 {
|
||||
go initStockData()
|
||||
|
Loading…
x
Reference in New Issue
Block a user