feat(stock):添加股票分组功能

- 新增股票分组相关接口和页面
- 实现分组添加、删除和股票移除功能
- 优化股票列表展示,支持按分组筛选
- 添加分组相关数据结构和 API
This commit is contained in:
ArvinLovegood 2025-04-03 17:21:07 +08:00
parent 9e5650617b
commit 512f9a0757
13 changed files with 548 additions and 78 deletions

78
app.go
View File

@ -101,6 +101,7 @@ func (a *App) startup(ctx context.Context) {
onExit(a) onExit(a)
}) })
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
} }
func (a *App) CheckUpdate() { func (a *App) CheckUpdate() {
@ -237,18 +238,19 @@ func (a *App) domReady(ctx context.Context) {
// logger.SugaredLogger.Infof("Edge浏览器已安装路径为: %s", path) // logger.SugaredLogger.Infof("Edge浏览器已安装路径为: %s", path)
// } // }
//}() //}()
followList := data.NewStockDataApi().GetFollowList() followList := data.NewStockDataApi().GetFollowList(0)
for _, follow := range *followList { for _, follow := range *followList {
if follow.Cron == "" { if follow.Cron == nil || *follow.Cron == "" {
continue continue
} }
entryID, err := a.cron.AddFunc(follow.Cron, a.AddCronTask(follow)) entryID, err := a.cron.AddFunc(*follow.Cron, a.AddCronTask(follow))
logger.SugaredLogger.Errorf("添加自动分析任务:%s cron=%s entryID:%v", follow.Name, follow.Cron, entryID)
a.cronEntrys[follow.StockCode] = entryID
if err != nil { if err != nil {
return logger.SugaredLogger.Errorf("添加自动分析任务失败:%s cron=%s entryID:%v", follow.Name, *follow.Cron, entryID)
continue
} }
a.cronEntrys[follow.StockCode] = entryID
} }
logger.SugaredLogger.Infof("domReady-cronEntrys:%+v", a.cronEntrys)
} }
@ -504,6 +506,7 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.CostVolume = follow.Volume //成本量 stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice stockData.AlarmPrice = follow.AlarmPrice
stockData.Groups = follow.Groups
//当前价格 //当前价格
price, _ := convertor.ToFloat(stockData.Price) price, _ := convertor.ToFloat(stockData.Price)
@ -592,17 +595,19 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
logger.SugaredLogger.Debugf("dialog:%s", dialog) logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" { if dialog == "No" {
return true return true
} else {
systray.Quit()
a.cron.Stop()
return false
} }
return false
} }
// shutdown is called at application termination // shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) { func (a *App) shutdown(ctx context.Context) {
defer PanicHandler() defer PanicHandler()
// Perform your teardown here // Perform your teardown here
systray.Quit() //os.Exit(0)
a.cron.Stop() logger.SugaredLogger.Infof("application shutdown Version:%s", Version)
os.Exit(0)
} }
// Greet returns a greeting for the given name // Greet returns a greeting for the given name
@ -612,7 +617,7 @@ func (a *App) Greet(stockCode string) *data.StockInfo {
follow := &data.FollowedStock{ follow := &data.FollowedStock{
StockCode: stockCode, StockCode: stockCode,
} }
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow) db.Dao.Model(follow).Where("stock_code = ?", stockCode).Preload("Groups").Preload("Groups.GroupInfo").First(follow)
stockInfo := getStockInfo(*follow) stockInfo := getStockInfo(*follow)
return stockInfo return stockInfo
} }
@ -625,8 +630,8 @@ func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode) return data.NewStockDataApi().UnFollow(stockCode)
} }
func (a *App) GetFollowList() *[]data.FollowedStock { func (a *App) GetFollowList(groupId int) *[]data.FollowedStock {
return data.NewStockDataApi().GetFollowList() return data.NewStockDataApi().GetFollowList(groupId)
} }
func (a *App) GetStockList(key string) []data.StockBasic { func (a *App) GetStockList(key string) []data.StockBasic {
@ -796,7 +801,7 @@ func getMsgTypeName(msgType int) string {
func onExit(a *App) { func onExit(a *App) {
// 清理操作 // 清理操作
logger.SugaredLogger.Infof("onExit") logger.SugaredLogger.Infof("systray onExit")
//systray.Quit() //systray.Quit()
//runtime.Quit(a.ctx) //runtime.Quit(a.ctx)
} }
@ -804,7 +809,7 @@ func onExit(a *App) {
func onReady(a *App) { func onReady(a *App) {
// 初始化操作 // 初始化操作
logger.SugaredLogger.Infof("onReady") logger.SugaredLogger.Infof("systray onReady")
systray.SetIcon(icon2) systray.SetIcon(icon2)
systray.SetTitle("go-stock") systray.SetTitle("go-stock")
systray.SetTooltip("go-stock 股票行情实时获取") systray.SetTooltip("go-stock 股票行情实时获取")
@ -988,3 +993,46 @@ func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
} }
time.Sleep(time.Second * 3) time.Sleep(time.Second * 3)
} }
func (a *App) AddGroup(group data.Group) string {
ok := data.NewStockGroupApi(db.Dao).AddGroup(group)
if ok {
return "添加成功"
} else {
return "添加失败"
}
}
func (a *App) GetGroupList() []data.Group {
return data.NewStockGroupApi(db.Dao).GetGroupList()
}
func (a *App) GetGroupStockList(groupId int) []data.GroupStock {
return data.NewStockGroupApi(db.Dao).GetGroupStockByGroupId(groupId)
}
func (a *App) AddStockGroup(groupId int, stockCode string) string {
ok := data.NewStockGroupApi(db.Dao).AddStockGroup(groupId, stockCode)
if ok {
return "添加成功"
} else {
return "添加失败"
}
}
func (a *App) RemoveStockGroup(code, name string, groupId int) string {
ok := data.NewStockGroupApi(db.Dao).RemoveStockGroup(code, name, groupId)
if ok {
return "移除成功"
} else {
return "移除失败"
}
}
func (a *App) RemoveGroup(groupId int) string {
ok := data.NewStockGroupApi(db.Dao).RemoveGroup(groupId)
if ok {
return "移除成功"
} else {
return "移除失败"
}
}

View File

@ -344,7 +344,7 @@ func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
//logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent) //logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent)
err := json.Unmarshal([]byte(htmlContent), &fundNetUnitValue) err := json.Unmarshal([]byte(htmlContent), &fundNetUnitValue)
if err != nil { if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error()) //logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
return return
} }
fund := &FollowedFund{ fund := &FollowedFund{

View File

@ -16,6 +16,7 @@ import (
"github.com/duke-git/lancet/v2/slice" "github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil" "github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/samber/lo"
"go-stock/backend/db" "go-stock/backend/db"
"go-stock/backend/logger" "go-stock/backend/logger"
"go-stock/backend/models" "go-stock/backend/models"
@ -90,6 +91,8 @@ type StockInfo struct {
Sort int64 `json:"sort"` //排序 Sort int64 `json:"sort"` //排序
AlarmChangePercent float64 `json:"alarmChangePercent"` AlarmChangePercent float64 `json:"alarmChangePercent"`
AlarmPrice float64 `json:"alarmPrice"` AlarmPrice float64 `json:"alarmPrice"`
Groups []GroupStock `gorm:"-:all"`
} }
func (receiver StockInfo) TableName() string { func (receiver StockInfo) TableName() string {
@ -162,8 +165,9 @@ type FollowedStock struct {
AlarmPrice float64 AlarmPrice float64
Time time.Time Time time.Time
Sort int64 Sort int64
Cron string Cron *string
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"` IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
Groups []GroupStock `gorm:"foreignKey:StockCode;references:StockCode"`
} }
func (receiver FollowedStock) TableName() string { func (receiver FollowedStock) TableName() string {
@ -429,9 +433,20 @@ func (receiver StockDataApi) SetStockAICron(cron string, stockCode string) {
db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("cron", cron) db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("cron", cron)
} }
func (receiver StockDataApi) GetFollowList() *[]FollowedStock { func (receiver StockDataApi) GetFollowList(groupId int) *[]FollowedStock {
logger.SugaredLogger.Infof("GetFollowList %d", groupId)
var result *[]FollowedStock var result *[]FollowedStock
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result) if groupId == 0 {
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)
} else {
infos := NewStockGroupApi(db.Dao).GetGroupStockByGroupId(groupId)
codes := lo.FlatMap(infos, func(info GroupStock, idx int) []string {
return []string{info.StockCode}
})
db.Dao.Model(&FollowedStock{}).Where("stock_code in ?", codes).Order("sort asc,time desc").Find(&result)
logger.SugaredLogger.Infof("GetFollowList %+v", result)
}
return result return result
} }

View File

@ -0,0 +1,80 @@
package data
import (
"go-stock/backend/db"
"gorm.io/gorm"
)
// @Author spark
// @Date 2025/4/3 11:18
// @Desc
// -----------------------------------------------------------------------------------
type Group struct {
gorm.Model
Name string `json:"name" gorm:"index"`
Sort int `json:"sort"`
}
func (Group) TableName() string {
return "stock_groups"
}
type GroupStock struct {
gorm.Model
StockCode string `json:"stockCode" gorm:"index"`
GroupId int `json:"groupId" gorm:"index"`
GroupInfo Group `json:"groupInfo" gorm:"foreignKey:GroupId;references:ID"`
}
func (GroupStock) TableName() string {
return "group_stock_info"
}
type StockGroupApi struct {
dao *gorm.DB
}
func NewStockGroupApi(dao *gorm.DB) *StockGroupApi {
return &StockGroupApi{dao: db.Dao}
}
func (receiver StockGroupApi) AddGroup(group Group) bool {
err := receiver.dao.Where("name = ?", group.Name).FirstOrCreate(&group).Updates(&Group{
Name: group.Name,
Sort: group.Sort,
}).Error
return err == nil
}
func (receiver StockGroupApi) GetGroupList() []Group {
var groups []Group
receiver.dao.Find(&groups)
return groups
}
func (receiver StockGroupApi) GetGroupStockByGroupId(groupId int) []GroupStock {
var stockGroup []GroupStock
receiver.dao.Preload("GroupInfo").Where("group_id = ?", groupId).Find(&stockGroup)
return stockGroup
}
func (receiver StockGroupApi) AddStockGroup(groupId int, stockCode string) bool {
err := receiver.dao.Where("group_id = ? and stock_code = ?", groupId, stockCode).FirstOrCreate(&GroupStock{
GroupId: groupId,
StockCode: stockCode,
}).Updates(&GroupStock{
GroupId: groupId,
StockCode: stockCode,
}).Error
return err == nil
}
func (receiver StockGroupApi) RemoveStockGroup(code string, name string, id int) bool {
err := receiver.dao.Where("group_id = ? and stock_code = ?", id, code).Delete(&GroupStock{}).Error
return err == nil
}
func (receiver StockGroupApi) RemoveGroup(id int) bool {
err := receiver.dao.Where("id = ?", id).Delete(&Group{}).Error
err = receiver.dao.Where("group_id = ?", id).Delete(&GroupStock{}).Error
return err == nil
}

View File

@ -20,7 +20,7 @@ func Init(sqlitePath string) {
Colorful: false, Colorful: false,
IgnoreRecordNotFoundError: true, IgnoreRecordNotFoundError: true,
ParameterizedQueries: false, ParameterizedQueries: false,
LogLevel: logger.Warn, LogLevel: logger.Info,
}, },
) )
var openDb *gorm.DB var openDb *gorm.DB

View File

@ -259,36 +259,36 @@ function deletePrompt(ID){
</script> </script>
<template> <template>
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px"> <n-flex justify="left" style="margin-top: 12px;padding-left: 12px;">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" > <n-form ref="formRef" :label-placement="'left'" :label-align="'left'" >
<n-grid :cols="24" :x-gap="24" style="text-align: left" > <n-grid :cols="24" :x-gap="24" style="text-align: left" :layout-shift-disabled="true">
<n-gi :span="24"> <n-gi :span="24">
<n-text type="primary" style="font-size: 25px;font-weight: bold">基础设置</n-text> <n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>
</n-gi> </n-gi>
<n-form-item-gi :span="10" label="Tushare api token" path="tushareToken" > <n-form-item-gi :span="10" label="Tushare &nbsp;&nbsp;Token" path="tushareToken" >
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable /> <n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" > <n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
<n-switch v-model:value="formValue.updateBasicInfoOnStart" /> <n-switch v-model:value="formValue.updateBasicInfoOnStart" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="5" label="数据刷新间隔:" path="refreshInterval" > <n-form-item-gi :span="4" label="数据刷新间隔:" path="refreshInterval" >
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)"> <n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
<template #suffix> <template #suffix>
</template> </template>
</n-input-number> </n-input-number>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="5" label="暗黑主题:" path="darkTheme" > <n-form-item-gi :span="6" label="暗黑主题:" path="darkTheme" >
<n-switch v-model:value="formValue.darkTheme" /> <n-switch v-model:value="formValue.darkTheme" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="22" label="浏览器路径:" path="browserPath" > <n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath" >
<n-input type="text" placeholder="浏览器路径" v-model:value="formValue.browserPath" clearable /> <n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
</n-form-item-gi> </n-form-item-gi>
</n-grid> </n-grid>
<n-grid :cols="24" :x-gap="24" style="text-align: left"> <n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24"> <n-gi :span="24">
<n-text type="primary" style="font-size: 25px;font-weight: bold">通知设置</n-text> <n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi> </n-gi>
<n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" > <n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" >
<n-switch v-model:value="formValue.dingPush.enable" /> <n-switch v-model:value="formValue.dingPush.enable" />
@ -310,7 +310,7 @@ function deletePrompt(ID){
<n-grid :cols="24" :x-gap="24" style="text-align: left;"> <n-grid :cols="24" :x-gap="24" style="text-align: left;">
<n-gi :span="24"> <n-gi :span="24">
<n-text type="primary" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text> <n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
</n-gi> </n-gi>
<n-form-item-gi :span="3" label="是否启用AI诊股" path="openAI.enable" > <n-form-item-gi :span="3" label="是否启用AI诊股" path="openAI.enable" >
<n-switch v-model:value="formValue.openAI.enable" /> <n-switch v-model:value="formValue.openAI.enable" />

View File

@ -15,7 +15,14 @@ import {
SetCostPriceAndVolume, SetCostPriceAndVolume,
SetStockSort, SetStockSort,
UnFollow, UnFollow,
ShareAnalysis, SaveAsMarkdown, GetPromptTemplates, SetStockAICron ShareAnalysis,
SaveAsMarkdown,
GetPromptTemplates,
SetStockAICron,
AddGroup,
GetGroupList,
AddStockGroup,
RemoveStockGroup, RemoveGroup
} from '../../wailsjs/go/main/App' } from '../../wailsjs/go/main/App'
import { import {
NAvatar, NAvatar,
@ -47,15 +54,15 @@ import {asBlob} from 'html-docx-js-typescript';
import vueDanmaku from 'vue3-danmaku' import vueDanmaku from 'vue3-danmaku'
import {keys, pad, padStart} from "lodash"; import {keys, pad, padStart} from "lodash";
import {models} from "../../wailsjs/go/models";
const danmus = ref([]) const danmus = ref([])
const ws = ref(null) const ws = ref(null)
const dialog = useDialog()
const toolbars = [0]; const toolbars = [0];
const handleProgress = (progress) => { const handleProgress = (progress) => {
console.log(`Export progress: ${progress.ratio * 100}%`); console.log(`Export progress: ${progress.ratio * 100}%`);
}; };
const enableEditor= ref(false) const enableEditor= ref(false)
const mdPreviewRef = ref(null) const mdPreviewRef = ref(null)
const mdEditorRef = ref(null) const mdEditorRef = ref(null)
const tipsRef = ref(null) const tipsRef = ref(null)
@ -65,6 +72,7 @@ const stocks=ref([])
const results=ref({}) const results=ref({})
const stockList=ref([]) const stockList=ref([])
const followList=ref([]) const followList=ref([])
const groupList=ref([])
const options=ref([]) const options=ref([])
const modalShow = ref(false) const modalShow = ref(false)
const modalShow2 = ref(false) const modalShow2 = ref(false)
@ -81,6 +89,7 @@ const formModel = ref({
sort:999, sort:999,
cron:"", cron:"",
}) })
const promptTemplates=ref([]) const promptTemplates=ref([])
const sysPromptOptions=ref([]) const sysPromptOptions=ref([])
const userPromptOptions=ref([]) const userPromptOptions=ref([])
@ -101,7 +110,7 @@ const data = reactive({
enableDanmu: false, enableDanmu: false,
darkTheme:false, darkTheme:false,
}) })
const currentGroupId=ref(0)
const theme=computed(() => { const theme=computed(() => {
return data.darkTheme ? 'dark' : 'light' return data.darkTheme ? 'dark' : 'light'
}) })
@ -115,15 +124,29 @@ const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/maste
const sortedResults = computed(() => { const sortedResults = computed(() => {
//console.log("computed",sortedResults.value) //console.log("computed",sortedResults.value)
const sortedKeys =keys(results.value).sort(); const sortedKeys =keys(results.value).sort();
console.log("sortedKeys",sortedKeys) //console.log("sortedKeys",sortedKeys)
const sortedObject = {}; const sortedObject = {};
sortedKeys.forEach(key => { sortedKeys.forEach(key => {
sortedObject[key] = results.value[key]; sortedObject[key] = results.value[key];
}); });
return sortedObject return sortedObject
}); });
const groupResults=computed(() => {
const group={}
for (const key in sortedResults.value) {
if(stocks.value.includes(sortedResults.value[key]['股票代码'])){
group[key]=sortedResults.value[key]
}
}
return group
})
onBeforeMount(()=>{ onBeforeMount(()=>{
GetGroupList().then(result => {
groupList.value=result
})
GetStockList("").then(result => { GetStockList("").then(result => {
stockList.value = result stockList.value = result
options.value=result.map(item => { options.value=result.map(item => {
@ -133,7 +156,7 @@ onBeforeMount(()=>{
} }
}) })
}) })
GetFollowList().then(result => { GetFollowList(currentGroupId.value).then(result => {
followList.value = result followList.value = result
for (const followedStock of result) { for (const followedStock of result) {
if(followedStock.StockCode.startsWith("us")){ if(followedStock.StockCode.startsWith("us")){
@ -495,6 +518,8 @@ async function updateData(result) {
//result.key=result.sort //result.key=result.sort
result.key=GetSortKey(result.sort,result["股票代码"]) result.key=GetSortKey(result.sort,result["股票代码"])
results.value[GetSortKey(result.sort,result["股票代码"])]=result results.value[GetSortKey(result.sort,result["股票代码"])]=result
console.log("updateData",result)
} }
@ -510,7 +535,7 @@ async function monitor() {
function GetSortKey(sort,code){ function GetSortKey(sort,code){
let sortKey= padStart(sort,8,'0')+"_"+code let sortKey= padStart(sort,8,'0')+"_"+code
console.log("GetSortKey:",sortKey) //console.log("GetSortKey:",sortKey)
return sortKey return sortKey
} }
@ -603,7 +628,7 @@ function updateCostPriceAndVolumeNew(code,price,volume,alarm,formModel){
SetCostPriceAndVolume(code,price,volume).then(result => { SetCostPriceAndVolume(code,price,volume).then(result => {
modalShow.value=false modalShow.value=false
message.success(result) message.success(result)
GetFollowList().then(result => { GetFollowList(currentGroupId.value).then(result => {
followList.value = result followList.value = result
for (const followedStock of result) { for (const followedStock of result) {
if (!stocks.value.includes(followedStock.StockCode)) { if (!stocks.value.includes(followedStock.StockCode)) {
@ -851,6 +876,73 @@ function share(code,name){
}) })
}) })
} }
const addTabModel=ref({
name: '',
sort: 1,
})
const addTabPane=ref(false)
function addTab(){
addTabPane.value=true
}
function saveTabPane(){
AddGroup(addTabModel.value).then(result => {
message.info(result)
addTabPane.value=false
})
}
function AddStockGroupInfo(groupId,code,name){
if(code.startsWith("gb_")){
code="us"+ code.replace("gb_", "").toLowerCase()
}
AddStockGroup(groupId,code).then(result => {
message.info(result)
GetGroupList().then(result => {
groupList.value=result
})
})
}
function updateTab(name){
currentGroupId.value=Number(name)
GetFollowList(currentGroupId.value).then(result => {
stocks.value=[]
console.log("GetFollowList",result)
followList.value = result
for (const followedStock of result) {
if(followedStock.StockCode.startsWith("us")){
followedStock.StockCode="gb_"+ followedStock.StockCode.replace("us", "").toLowerCase()
}
//console.log("followList",followedStock.StockCode)
stocks.value.push(followedStock.StockCode)
}
monitor()
message.destroyAll()
})
}
function delTab(name){
let infos=groupList.value=groupList.value.filter(item => item.ID === Number(name))
dialog.create({
title: '删除分组',
type: 'warning',
content: '确定要删除['+infos[0].name+']分组吗?分组数据将不能恢复哟!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
RemoveGroup(name).then(result => {
message.info(result)
GetGroupList().then(result => {
groupList.value=result
})
})
}
})
}
function delStockGroup(code,name,groupId){
RemoveStockGroup(code,name,groupId).then(result => {
updateTab(groupId)
message.info(result)
})
}
</script> </script>
<template> <template>
@ -861,8 +953,10 @@ function share(code,name){
</n-gradient-text> </n-gradient-text>
</template> </template>
</vue-danmaku> </vue-danmaku>
<n-grid :x-gap="8" :cols="3" :y-gap="8" > <n-tabs type="card" animated addable @add="addTab" @update-value="updateTab" placement="top" @close="(key)=>{delTab(key)}">
<n-gi :id="result['股票代码']+'_gi'" v-for="result in sortedResults" style="margin-left: 2px;" > <n-tab-pane name="0" tab="全部">
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
<n-gi :id="result['股票代码']+'_gi'" v-for="result in sortedResults" style="margin-left: 2px;" >
<n-card :data-sort="result.sort" :id="result['股票代码']" :data-code="result['股票代码']" :bordered="true" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)"> <n-card :data-sort="result.sort" :id="result['股票代码']" :data-code="result['股票代码']" :bordered="true" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
<n-grid :cols="1" :y-gap="6"> <n-grid :cols="1" :y-gap="6">
<n-gi> <n-gi>
@ -916,11 +1010,82 @@ function share(code,name){
<n-button size="tiny" type="success" @click="showFenshi(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="error" @click="showK(result['股票代码'],result['股票名称'])"> 日K </n-button>
<n-button size="tiny" type="warning" @click="search(result['股票代码'],result['股票名称'])"> 详情 </n-button> <n-button size="tiny" type="warning" @click="search(result['股票代码'],result['股票名称'])"> 详情 </n-button>
<n-dropdown trigger="click" :options="groupList" key-field="ID" label-field="name" @select="(groupId) => AddStockGroupInfo(groupId,result['股票代码'],result['股票名称'])">
<n-button type="success" size="tiny">设置分组</n-button>
</n-dropdown>
</n-flex> </n-flex>
</template> </template>
</n-card > </n-card >
</n-gi> </n-gi>
</n-grid> </n-grid>
</n-tab-pane>
<n-tab-pane closable v-for="group in groupList" :group-id="group.ID" :name="group.ID" :tab="group.name">
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
<n-gi :id="result['股票代码']+'_gi'" v-for="result in groupResults" style="margin-left: 2px;" >
<n-card :data-sort="result.sort" :id="result['股票代码']" :data-code="result['股票代码']" :bordered="true" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
<n-grid :cols="1" :y-gap="6">
<n-gi>
<n-text :type="result.type" >
<n-number-animation :duration="1000" :precision="2" :from="result['上次当前价格']" :to="Number(result['当前价格'])" />
<n-tag size="small" :type="result.type" :bordered="false" v-if="result['盘前盘后']>0">({{result['盘前盘后']}} {{result['盘前盘后涨跌幅']}}%)</n-tag>
</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>&nbsp;
<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" :bordered="false">{{result['股票代码']}}</n-tag>&nbsp;
<n-button size="tiny" secondary type="primary" @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
取消关注
</n-button>&nbsp;
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
AI分析
</n-button>&nbsp;
<n-button secondary type="error" size="tiny" @click="delStockGroup(result['股票代码'],result['股票名称'],group.ID)">移出分组</n-button>
</template>
<template #footer>
<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-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-dropdown trigger="click" :options="groupList" key-field="ID" label-field="name" @select="(groupId) => AddStockGroupInfo(groupId,result['股票代码'],result['股票名称'])">
<n-button type="success" size="tiny">设置分组</n-button>
</n-dropdown>
</n-flex>
</template>
</n-card >
</n-gi>
</n-grid>
</n-tab-pane>
</n-tabs>
<div style="position: fixed;bottom: 18px;right:5px;z-index: 10;width: 400px"> <div style="position: fixed;bottom: 18px;right:5px;z-index: 10;width: 400px">
<!-- <n-card :bordered="false">--> <!-- <n-card :bordered="false">-->
<n-input-group > <n-input-group >
@ -991,6 +1156,31 @@ function share(code,name){
</template> </template>
</n-modal> </n-modal>
<n-modal v-model:show="addTabPane" title="添加分组" style="width: 400px;text-align: left" :preset="'card'">
<n-form
:model="addTabModel"
size="medium"
label-placement="left"
> <n-grid :cols="2" >
<n-form-item-gi label="分组名称:" path="name" :span="5">
<n-input v-model:value="addTabModel.name" style="width: 100%" placeholder="请输入分组名称" />
</n-form-item-gi>
<n-form-item-gi label="分组排序:" path="sort" :span="5">
<n-input-number v-model:value="addTabModel.sort" style="width: 100%" min="0" placeholder="请输入分组排序值" ></n-input-number>
</n-form-item-gi>
</n-grid>
</n-form>
<template #footer>
<n-flex justify="end">
<n-button type="primary" @click="saveTabPane">
保存
</n-button>
<n-button type="warning" @click="addTabPane=false">
取消
</n-button>
</n-flex>
</template>
</n-modal>
<n-modal v-model:show="modalShow2" :title="data.name" style="width: 600px" :preset="'card'"> <n-modal v-model:show="modalShow2" :title="data.name" style="width: 600px" :preset="'card'">
<n-image :src="data.fenshiURL" /> <n-image :src="data.fenshiURL" />
</n-modal> </n-modal>

View File

@ -1,4 +1,4 @@
import { createMemoryHistory, createRouter } from 'vue-router' import {createMemoryHistory, createRouter, createWebHashHistory} from 'vue-router'
import stockView from '../components/stock.vue' import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue' import settingsView from '../components/settings.vue'
@ -13,7 +13,7 @@ const routes = [
] ]
const router = createRouter({ const router = createRouter({
history: createMemoryHistory(), history: createWebHashHistory(),
routes, routes,
}) })

View File

@ -5,8 +5,12 @@ import {models} from '../models';
export function AddCronTask(arg1:data.FollowedStock):Promise<any>; export function AddCronTask(arg1:data.FollowedStock):Promise<any>;
export function AddGroup(arg1:data.Group):Promise<string>;
export function AddPrompt(arg1:models.Prompt):Promise<string>; export function AddPrompt(arg1:models.Prompt):Promise<string>;
export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
export function CheckUpdate():Promise<void>; export function CheckUpdate():Promise<void>;
export function DelPrompt(arg1:number):Promise<string>; export function DelPrompt(arg1:number):Promise<string>;
@ -21,10 +25,14 @@ export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult
export function GetConfig():Promise<data.Settings>; export function GetConfig():Promise<data.Settings>;
export function GetFollowList():Promise<any>; export function GetFollowList(arg1:number):Promise<any>;
export function GetFollowedFund():Promise<Array<data.FollowedFund>>; export function GetFollowedFund():Promise<Array<data.FollowedFund>>;
export function GetGroupList():Promise<Array<data.Group>>;
export function GetGroupStockList(arg1:number):Promise<Array<data.GroupStock>>;
export function GetPromptTemplates(arg1:string,arg2:string):Promise<any>; export function GetPromptTemplates(arg1:string,arg2:string):Promise<any>;
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>; export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
@ -37,6 +45,10 @@ export function Greet(arg1:string):Promise<data.StockInfo>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>; export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
export function RemoveGroup(arg1:number):Promise<string>;
export function RemoveStockGroup(arg1:string,arg2:string,arg3:number):Promise<string>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>; export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>; export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;

View File

@ -6,10 +6,18 @@ export function AddCronTask(arg1) {
return window['go']['main']['App']['AddCronTask'](arg1); return window['go']['main']['App']['AddCronTask'](arg1);
} }
export function AddGroup(arg1) {
return window['go']['main']['App']['AddGroup'](arg1);
}
export function AddPrompt(arg1) { export function AddPrompt(arg1) {
return window['go']['main']['App']['AddPrompt'](arg1); return window['go']['main']['App']['AddPrompt'](arg1);
} }
export function AddStockGroup(arg1, arg2) {
return window['go']['main']['App']['AddStockGroup'](arg1, arg2);
}
export function CheckUpdate() { export function CheckUpdate() {
return window['go']['main']['App']['CheckUpdate'](); return window['go']['main']['App']['CheckUpdate']();
} }
@ -38,14 +46,22 @@ export function GetConfig() {
return window['go']['main']['App']['GetConfig'](); return window['go']['main']['App']['GetConfig']();
} }
export function GetFollowList() { export function GetFollowList(arg1) {
return window['go']['main']['App']['GetFollowList'](); return window['go']['main']['App']['GetFollowList'](arg1);
} }
export function GetFollowedFund() { export function GetFollowedFund() {
return window['go']['main']['App']['GetFollowedFund'](); return window['go']['main']['App']['GetFollowedFund']();
} }
export function GetGroupList() {
return window['go']['main']['App']['GetGroupList']();
}
export function GetGroupStockList(arg1) {
return window['go']['main']['App']['GetGroupStockList'](arg1);
}
export function GetPromptTemplates(arg1, arg2) { export function GetPromptTemplates(arg1, arg2) {
return window['go']['main']['App']['GetPromptTemplates'](arg1, arg2); return window['go']['main']['App']['GetPromptTemplates'](arg1, arg2);
} }
@ -70,6 +86,14 @@ export function NewChatStream(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4); return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
} }
export function RemoveGroup(arg1) {
return window['go']['main']['App']['RemoveGroup'](arg1);
}
export function RemoveStockGroup(arg1, arg2, arg3) {
return window['go']['main']['App']['RemoveStockGroup'](arg1, arg2, arg3);
}
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) { export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5); return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
} }

View File

@ -142,41 +142,29 @@ export namespace data {
return a; return a;
} }
} }
export class FollowedStock { export class Group {
StockCode: string; ID: number;
Name: string;
Volume: number;
CostPrice: number;
Price: number;
PriceChange: number;
ChangePercent: number;
AlarmChangePercent: number;
AlarmPrice: number;
// Go type: time // Go type: time
Time: any; CreatedAt: any;
Sort: number; // Go type: time
Cron: string; UpdatedAt: any;
IsDel: number; // Go type: gorm
DeletedAt: any;
name: string;
sort: number;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new FollowedStock(source); return new Group(source);
} }
constructor(source: any = {}) { constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source); if ('string' === typeof source) source = JSON.parse(source);
this.StockCode = source["StockCode"]; this.ID = source["ID"];
this.Name = source["Name"]; this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.Volume = source["Volume"]; this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.CostPrice = source["CostPrice"]; this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.Price = source["Price"]; this.name = source["name"];
this.PriceChange = source["PriceChange"]; this.sort = source["sort"];
this.ChangePercent = source["ChangePercent"];
this.AlarmChangePercent = source["AlarmChangePercent"];
this.AlarmPrice = source["AlarmPrice"];
this.Time = this.convertValues(source["Time"], null);
this.Sort = source["Sort"];
this.Cron = source["Cron"];
this.IsDel = source["IsDel"];
} }
convertValues(a: any, classs: any, asMap: boolean = false): any { convertValues(a: any, classs: any, asMap: boolean = false): any {
@ -197,6 +185,110 @@ export namespace data {
return a; return a;
} }
} }
export class GroupStock {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
stockCode: string;
groupId: number;
groupInfo: Group;
static createFrom(source: any = {}) {
return new GroupStock(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.groupId = source["groupId"];
this.groupInfo = this.convertValues(source["groupInfo"], Group);
}
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 FollowedStock {
StockCode: string;
Name: string;
Volume: number;
CostPrice: number;
Price: number;
PriceChange: number;
ChangePercent: number;
AlarmChangePercent: number;
AlarmPrice: number;
// Go type: time
Time: any;
Sort: number;
Cron?: string;
IsDel: number;
Groups: GroupStock[];
static createFrom(source: any = {}) {
return new FollowedStock(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.StockCode = source["StockCode"];
this.Name = source["Name"];
this.Volume = source["Volume"];
this.CostPrice = source["CostPrice"];
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.Cron = source["Cron"];
this.IsDel = source["IsDel"];
this.Groups = this.convertValues(source["Groups"], GroupStock);
}
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 { export class Settings {
ID: number; ID: number;
@ -413,6 +505,7 @@ export namespace data {
sort: number; sort: number;
alarmChangePercent: number; alarmChangePercent: number;
alarmPrice: number; alarmPrice: number;
Groups: GroupStock[];
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new StockInfo(source); return new StockInfo(source);
@ -473,6 +566,7 @@ export namespace data {
this.sort = source["sort"]; this.sort = source["sort"];
this.alarmChangePercent = source["alarmChangePercent"]; this.alarmChangePercent = source["alarmChangePercent"];
this.alarmPrice = source["alarmPrice"]; this.alarmPrice = source["alarmPrice"];
this.Groups = this.convertValues(source["Groups"], GroupStock);
} }
convertValues(a: any, classs: any, asMap: boolean = false): any { convertValues(a: any, classs: any, asMap: boolean = false): any {

2
go.mod
View File

@ -14,6 +14,7 @@ require (
github.com/go-resty/resty/v2 v2.16.2 github.com/go-resty/resty/v2 v2.16.2
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.49.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wailsapp/wails/v2 v2.10.1 github.com/wailsapp/wails/v2 v2.10.1
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
@ -59,7 +60,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect

View File

@ -66,6 +66,13 @@ func main() {
db.Dao.AutoMigrate(&data.FollowedFund{}) db.Dao.AutoMigrate(&data.FollowedFund{})
db.Dao.AutoMigrate(&data.FundBasic{}) db.Dao.AutoMigrate(&data.FundBasic{})
db.Dao.AutoMigrate(&models.PromptTemplate{}) db.Dao.AutoMigrate(&models.PromptTemplate{})
db.Dao.AutoMigrate(&data.Group{})
db.Dao.AutoMigrate(&data.GroupStock{})
//db.Dao.Model(&data.Group{}).Where("id = ?", 0).FirstOrCreate(&data.Group{
// Name: "默认分组",
// Sort: 0,
//})
if stocksBin != nil && len(stocksBin) > 0 { if stocksBin != nil && len(stocksBin) > 0 {
go initStockData() go initStockData()