feat(fund):添加基金监控和查询功能

- 新增基金数据 API,实现基金信息爬取和数据库操作
- 添加基金监控逻辑,定期更新基金净值信息
- 实现基金列表查询、关注和取消关注功能
- 新增基金相关前端组件,展示基金信息和操作
This commit is contained in:
ArvinLovegood 2025-03-09 16:35:53 +08:00
parent 1b31ff04df
commit 9a6e210bae
12 changed files with 829 additions and 10 deletions

37
app.go
View File

@ -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)
}

View 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)
}
}
}

View 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")
}

View File

@ -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)
}

View File

@ -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),

View 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>&nbsp;
<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"/> &nbsp;关注
</n-button>
<n-button type="error" @click="SendDanmu" v-if="data.enableDanmu">
<n-icon :component="ChatboxOutline"/> &nbsp;发送弹幕
</n-button>
</n-input-group>
</div>
</template>
<style scoped>
</style>

View File

@ -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"/>&nbsp;<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"/> &nbsp;关注该股票
<n-icon :component="Add"/> &nbsp;关注
</n-button>
<n-button type="error" @click="SendDanmu" v-if="data.enableDanmu">
<n-icon :component="ChatboxOutline"/> &nbsp;发送弹幕

View File

@ -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' },
]

View File

@ -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>;

View File

@ -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);
}

View File

@ -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

View File

@ -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()