mirror of
https://github.com/ArvinLovegood/go-stock.git
synced 2025-07-19 00:00:09 +08:00
feat(stock):添加股票分组功能
- 新增股票分组相关接口和页面 - 实现分组添加、删除和股票移除功能 - 优化股票列表展示,支持按分组筛选 - 添加分组相关数据结构和 API
This commit is contained in:
parent
9e5650617b
commit
512f9a0757
78
app.go
78
app.go
@ -101,6 +101,7 @@ func (a *App) startup(ctx context.Context) {
|
||||
onExit(a)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
|
||||
}
|
||||
|
||||
func (a *App) CheckUpdate() {
|
||||
@ -237,18 +238,19 @@ func (a *App) domReady(ctx context.Context) {
|
||||
// logger.SugaredLogger.Infof("Edge浏览器已安装,路径为: %s", path)
|
||||
// }
|
||||
//}()
|
||||
followList := data.NewStockDataApi().GetFollowList()
|
||||
followList := data.NewStockDataApi().GetFollowList(0)
|
||||
for _, follow := range *followList {
|
||||
if follow.Cron == "" {
|
||||
if follow.Cron == nil || *follow.Cron == "" {
|
||||
continue
|
||||
}
|
||||
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
|
||||
entryID, err := a.cron.AddFunc(*follow.Cron, a.AddCronTask(follow))
|
||||
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.AlarmChangePercent = follow.AlarmChangePercent
|
||||
stockData.AlarmPrice = follow.AlarmPrice
|
||||
stockData.Groups = follow.Groups
|
||||
|
||||
//当前价格
|
||||
price, _ := convertor.ToFloat(stockData.Price)
|
||||
@ -592,17 +595,19 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
systray.Quit()
|
||||
a.cron.Stop()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
// Perform your teardown here
|
||||
systray.Quit()
|
||||
a.cron.Stop()
|
||||
os.Exit(0)
|
||||
//os.Exit(0)
|
||||
logger.SugaredLogger.Infof("application shutdown Version:%s", Version)
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
@ -612,7 +617,7 @@ func (a *App) Greet(stockCode string) *data.StockInfo {
|
||||
follow := &data.FollowedStock{
|
||||
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)
|
||||
return stockInfo
|
||||
}
|
||||
@ -625,8 +630,8 @@ func (a *App) UnFollow(stockCode string) string {
|
||||
return data.NewStockDataApi().UnFollow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) GetFollowList() *[]data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList()
|
||||
func (a *App) GetFollowList(groupId int) *[]data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList(groupId)
|
||||
}
|
||||
|
||||
func (a *App) GetStockList(key string) []data.StockBasic {
|
||||
@ -796,7 +801,7 @@ func getMsgTypeName(msgType int) string {
|
||||
|
||||
func onExit(a *App) {
|
||||
// 清理操作
|
||||
logger.SugaredLogger.Infof("onExit")
|
||||
logger.SugaredLogger.Infof("systray onExit")
|
||||
//systray.Quit()
|
||||
//runtime.Quit(a.ctx)
|
||||
}
|
||||
@ -804,7 +809,7 @@ func onExit(a *App) {
|
||||
func onReady(a *App) {
|
||||
|
||||
// 初始化操作
|
||||
logger.SugaredLogger.Infof("onReady")
|
||||
logger.SugaredLogger.Infof("systray onReady")
|
||||
systray.SetIcon(icon2)
|
||||
systray.SetTitle("go-stock")
|
||||
systray.SetTooltip("go-stock 股票行情实时获取")
|
||||
@ -988,3 +993,46 @@ func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
}
|
||||
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 "移除失败"
|
||||
}
|
||||
}
|
||||
|
@ -344,7 +344,7 @@ func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
|
||||
//logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent)
|
||||
err := json.Unmarshal([]byte(htmlContent), &fundNetUnitValue)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
//logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
fund := &FollowedFund{
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/samber/lo"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
@ -90,6 +91,8 @@ type StockInfo struct {
|
||||
Sort int64 `json:"sort"` //排序
|
||||
AlarmChangePercent float64 `json:"alarmChangePercent"`
|
||||
AlarmPrice float64 `json:"alarmPrice"`
|
||||
|
||||
Groups []GroupStock `gorm:"-:all"`
|
||||
}
|
||||
|
||||
func (receiver StockInfo) TableName() string {
|
||||
@ -162,8 +165,9 @@ type FollowedStock struct {
|
||||
AlarmPrice float64
|
||||
Time time.Time
|
||||
Sort int64
|
||||
Cron string
|
||||
Cron *string
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
Groups []GroupStock `gorm:"foreignKey:StockCode;references:StockCode"`
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
func (receiver StockDataApi) GetFollowList() *[]FollowedStock {
|
||||
func (receiver StockDataApi) GetFollowList(groupId int) *[]FollowedStock {
|
||||
logger.SugaredLogger.Infof("GetFollowList %d", groupId)
|
||||
|
||||
var result *[]FollowedStock
|
||||
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
|
||||
}
|
||||
|
||||
|
80
backend/data/stock_group_api.go
Normal file
80
backend/data/stock_group_api.go
Normal 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
|
||||
|
||||
}
|
@ -20,7 +20,7 @@ func Init(sqlitePath string) {
|
||||
Colorful: false,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
ParameterizedQueries: false,
|
||||
LogLevel: logger.Warn,
|
||||
LogLevel: logger.Info,
|
||||
},
|
||||
)
|
||||
var openDb *gorm.DB
|
||||
|
@ -259,36 +259,36 @@ function deletePrompt(ID){
|
||||
</script>
|
||||
|
||||
<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-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-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-form-item-gi :span="10" label="Tushare api token:" path="tushareToken" >
|
||||
<n-form-item-gi :span="10" label="Tushare Token:" path="tushareToken" >
|
||||
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
|
||||
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
|
||||
</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="请输入数据刷新间隔(秒)">
|
||||
<template #suffix>
|
||||
秒
|
||||
</template>
|
||||
</n-input-number>
|
||||
</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-form-item-gi>
|
||||
<n-form-item-gi :span="22" label="浏览器路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器路径" v-model:value="formValue.browserPath" clearable />
|
||||
<n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<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-form-item-gi :span="6" label="是否启用钉钉推送:" path="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-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-form-item-gi :span="3" label="是否启用AI诊股:" path="openAI.enable" >
|
||||
<n-switch v-model:value="formValue.openAI.enable" />
|
||||
|
@ -15,7 +15,14 @@ import {
|
||||
SetCostPriceAndVolume,
|
||||
SetStockSort,
|
||||
UnFollow,
|
||||
ShareAnalysis, SaveAsMarkdown, GetPromptTemplates, SetStockAICron
|
||||
ShareAnalysis,
|
||||
SaveAsMarkdown,
|
||||
GetPromptTemplates,
|
||||
SetStockAICron,
|
||||
AddGroup,
|
||||
GetGroupList,
|
||||
AddStockGroup,
|
||||
RemoveStockGroup, RemoveGroup
|
||||
} from '../../wailsjs/go/main/App'
|
||||
import {
|
||||
NAvatar,
|
||||
@ -47,15 +54,15 @@ import {asBlob} from 'html-docx-js-typescript';
|
||||
|
||||
import vueDanmaku from 'vue3-danmaku'
|
||||
import {keys, pad, padStart} from "lodash";
|
||||
import {models} from "../../wailsjs/go/models";
|
||||
const danmus = ref([])
|
||||
const ws = ref(null)
|
||||
|
||||
const dialog = useDialog()
|
||||
const toolbars = [0];
|
||||
const handleProgress = (progress) => {
|
||||
console.log(`Export progress: ${progress.ratio * 100}%`);
|
||||
};
|
||||
const enableEditor= ref(false)
|
||||
|
||||
const mdPreviewRef = ref(null)
|
||||
const mdEditorRef = ref(null)
|
||||
const tipsRef = ref(null)
|
||||
@ -65,6 +72,7 @@ const stocks=ref([])
|
||||
const results=ref({})
|
||||
const stockList=ref([])
|
||||
const followList=ref([])
|
||||
const groupList=ref([])
|
||||
const options=ref([])
|
||||
const modalShow = ref(false)
|
||||
const modalShow2 = ref(false)
|
||||
@ -81,6 +89,7 @@ const formModel = ref({
|
||||
sort:999,
|
||||
cron:"",
|
||||
})
|
||||
|
||||
const promptTemplates=ref([])
|
||||
const sysPromptOptions=ref([])
|
||||
const userPromptOptions=ref([])
|
||||
@ -101,7 +110,7 @@ const data = reactive({
|
||||
enableDanmu: false,
|
||||
darkTheme:false,
|
||||
})
|
||||
|
||||
const currentGroupId=ref(0)
|
||||
const theme=computed(() => {
|
||||
return data.darkTheme ? 'dark' : 'light'
|
||||
})
|
||||
@ -115,7 +124,7 @@ const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/maste
|
||||
const sortedResults = computed(() => {
|
||||
//console.log("computed",sortedResults.value)
|
||||
const sortedKeys =keys(results.value).sort();
|
||||
console.log("sortedKeys",sortedKeys)
|
||||
//console.log("sortedKeys",sortedKeys)
|
||||
const sortedObject = {};
|
||||
sortedKeys.forEach(key => {
|
||||
sortedObject[key] = results.value[key];
|
||||
@ -123,7 +132,21 @@ const sortedResults = computed(() => {
|
||||
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(()=>{
|
||||
|
||||
GetGroupList().then(result => {
|
||||
groupList.value=result
|
||||
})
|
||||
GetStockList("").then(result => {
|
||||
stockList.value = result
|
||||
options.value=result.map(item => {
|
||||
@ -133,7 +156,7 @@ onBeforeMount(()=>{
|
||||
}
|
||||
})
|
||||
})
|
||||
GetFollowList().then(result => {
|
||||
GetFollowList(currentGroupId.value).then(result => {
|
||||
followList.value = result
|
||||
for (const followedStock of result) {
|
||||
if(followedStock.StockCode.startsWith("us")){
|
||||
@ -495,6 +518,8 @@ async function updateData(result) {
|
||||
//result.key=result.sort
|
||||
result.key=GetSortKey(result.sort,result["股票代码"])
|
||||
results.value[GetSortKey(result.sort,result["股票代码"])]=result
|
||||
|
||||
console.log("updateData",result)
|
||||
}
|
||||
|
||||
|
||||
@ -510,7 +535,7 @@ async function monitor() {
|
||||
|
||||
function GetSortKey(sort,code){
|
||||
let sortKey= padStart(sort,8,'0')+"_"+code
|
||||
console.log("GetSortKey:",sortKey)
|
||||
//console.log("GetSortKey:",sortKey)
|
||||
return sortKey
|
||||
}
|
||||
|
||||
@ -603,7 +628,7 @@ function updateCostPriceAndVolumeNew(code,price,volume,alarm,formModel){
|
||||
SetCostPriceAndVolume(code,price,volume).then(result => {
|
||||
modalShow.value=false
|
||||
message.success(result)
|
||||
GetFollowList().then(result => {
|
||||
GetFollowList(currentGroupId.value).then(result => {
|
||||
followList.value = result
|
||||
for (const followedStock of result) {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -861,6 +953,8 @@ function share(code,name){
|
||||
</n-gradient-text>
|
||||
</template>
|
||||
</vue-danmaku>
|
||||
<n-tabs type="card" animated addable @add="addTab" @update-value="updateTab" placement="top" @close="(key)=>{delTab(key)}">
|
||||
<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)">
|
||||
@ -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="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-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>
|
||||
<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>
|
||||
<n-button size="tiny" secondary type="primary" @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
|
||||
取消关注
|
||||
</n-button>
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||||
AI分析
|
||||
</n-button>
|
||||
<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">
|
||||
<!-- <n-card :bordered="false">-->
|
||||
<n-input-group >
|
||||
@ -991,6 +1156,31 @@ function share(code,name){
|
||||
</template>
|
||||
</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-image :src="data.fenshiURL" />
|
||||
</n-modal>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import {createMemoryHistory, createRouter, createWebHashHistory} from 'vue-router'
|
||||
|
||||
import stockView from '../components/stock.vue'
|
||||
import settingsView from '../components/settings.vue'
|
||||
@ -13,7 +13,7 @@ const routes = [
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
|
14
frontend/wailsjs/go/main/App.d.ts
vendored
14
frontend/wailsjs/go/main/App.d.ts
vendored
@ -5,8 +5,12 @@ import {models} from '../models';
|
||||
|
||||
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 AddStockGroup(arg1:number,arg2:string):Promise<string>;
|
||||
|
||||
export function CheckUpdate():Promise<void>;
|
||||
|
||||
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 GetFollowList():Promise<any>;
|
||||
export function GetFollowList(arg1:number):Promise<any>;
|
||||
|
||||
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 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 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 SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
|
||||
|
@ -6,10 +6,18 @@ export function AddCronTask(arg1) {
|
||||
return window['go']['main']['App']['AddCronTask'](arg1);
|
||||
}
|
||||
|
||||
export function AddGroup(arg1) {
|
||||
return window['go']['main']['App']['AddGroup'](arg1);
|
||||
}
|
||||
|
||||
export function 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() {
|
||||
return window['go']['main']['App']['CheckUpdate']();
|
||||
}
|
||||
@ -38,14 +46,22 @@ export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
|
||||
export function GetFollowList() {
|
||||
return window['go']['main']['App']['GetFollowList']();
|
||||
export function GetFollowList(arg1) {
|
||||
return window['go']['main']['App']['GetFollowList'](arg1);
|
||||
}
|
||||
|
||||
export function 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
@ -142,41 +142,29 @@ export namespace data {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class FollowedStock {
|
||||
StockCode: string;
|
||||
Name: string;
|
||||
Volume: number;
|
||||
CostPrice: number;
|
||||
Price: number;
|
||||
PriceChange: number;
|
||||
ChangePercent: number;
|
||||
AlarmChangePercent: number;
|
||||
AlarmPrice: number;
|
||||
export class Group {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
Time: any;
|
||||
Sort: number;
|
||||
Cron: string;
|
||||
IsDel: number;
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
name: string;
|
||||
sort: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedStock(source);
|
||||
return new Group(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.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.name = source["name"];
|
||||
this.sort = source["sort"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@ -197,6 +185,110 @@ export namespace data {
|
||||
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 {
|
||||
ID: number;
|
||||
@ -413,6 +505,7 @@ export namespace data {
|
||||
sort: number;
|
||||
alarmChangePercent: number;
|
||||
alarmPrice: number;
|
||||
Groups: GroupStock[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StockInfo(source);
|
||||
@ -473,6 +566,7 @@ export namespace data {
|
||||
this.sort = source["sort"];
|
||||
this.alarmChangePercent = source["alarmChangePercent"];
|
||||
this.alarmPrice = source["alarmPrice"];
|
||||
this.Groups = this.convertValues(source["Groups"], GroupStock);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
|
2
go.mod
2
go.mod
@ -14,6 +14,7 @@ require (
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
go.uber.org/zap v1.27.0
|
||||
@ -59,7 +60,6 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
|
7
main.go
7
main.go
@ -66,6 +66,13 @@ func main() {
|
||||
db.Dao.AutoMigrate(&data.FollowedFund{})
|
||||
db.Dao.AutoMigrate(&data.FundBasic{})
|
||||
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 {
|
||||
go initStockData()
|
||||
|
Loading…
x
Reference in New Issue
Block a user