Compare commits

...

80 Commits

Author SHA1 Message Date
spark
869eced99e refactor(backend): 优化 API 客户端配置并调整日志输出
- 为 OpenAI API 客户端添加重试次数和超时设置
- 修改 OpenAI API 客户端初始化,设置基础 URL
- 优化 OpenAI API 响应数据的处理逻辑
- 为 stock_data API 客户端添加重试次数设置
- 在 stock_data API 中添加日志和错误处理
2025-02-03 13:50:13 +08:00
spark
f5aa70bf61 feat(main): 调整窗口最大宽度和高度并启用默认上下文菜单
- 将窗口最大宽度从 1280调整为 1920
- 启用默认上下文菜单
2025-02-01 13:18:43 +08:00
spark
71289d1408 feat(openai): 添加自定义 prompt 功能
- 更新前端设置组件,增加自定义 prompt 输入框
- 更新后端设置 API,支持保存和读取 prompt 配置
2025-02-01 11:32:38 +08:00
spark
f6297d224c refactor(frontend): 优化AI股票分析用户体验
- 在股票检测过程中添加 loading 状态
2025-01-31 16:26:36 +08:00
spark
0bfa50e2b6 feat(frontend): 优化 AI 分析功能
- 添加 loading 状态和DONE消息处理
- 改进消息提示和销毁逻辑
- 优化 AI 分析结果的展示
- 调整 API测试日志输出
2025-01-31 15:50:47 +08:00
spark
0d182b923f build: 更新 wails 构建动作
- 将 dAppServer/wails-build-action 替换为 ArvinLovegood/wails-build-action
- 指定使用2.3 版本的 wails 构建动作
2025-01-31 13:46:29 +08:00
spark
7f19d00a23 build: 更新 wails 构建动作
- 将 dAppServer/wails-build-action 替换为 ArvinLovegood/wails-build-action
- 指定使用2.3 版本的 wails 构建动作
2025-01-31 13:44:49 +08:00
spark
f514a0083d build: 更新 wails 构建动作引用
- 将 dAppServer/wails-build-action@latest替换为 dAppServer/wails-build-action
- 此修改旨在解决特定版本冲突问题
2025-01-31 13:40:51 +08:00
spark
7fbe178c78 build: 更新 Wails 构建动作版本
- 将 dAppServer/wails-build-action 版本从 v2.2 修改为 latest
- 此更改确保使用最新版本的 Wails 构建动作,可能包含未记录的最新功能和修复
2025-01-31 13:38:13 +08:00
spark
40fe30ce2f refactor(data): 重构股票数据获取方法
- 优化了股票价格信息的获取流程,增加了对关键元素的等待和检查
- 改进了搜索结果页面的处理,使用更准确的选择器进行等待
- 删除了不必要的测试函数,整合了相关的测试用例
2025-01-31 13:02:04 +08:00
spark
1751be729b feat(frontend): 添加 Tushare 接口 token 配置功能
- 在前端设置页面增加 Tushare api token 输入框
- 在后端 Settings 结构体中添加 TushareToken 字段- 更新相关 API 调用,使用配置的 TushareToken
2025-01-26 21:41:48 +08:00
spark
d82ace220a ci: 注释掉 Linux 构建
- 不在支持Linux平台,专注Windows平台
- 在 GitHub Actions 配置文件中注释掉了 Linux构建相关配置
- 此修改将阻止在 Ubuntu 系统上进行 go-stock-linux-amd64 的构建
2025-01-26 11:53:00 +08:00
spark
9c51ecde2f ci: 更新作者邮箱地址
- 将作者邮箱从 "wzl@huazx.cn" 修改为 "sparkmemory@163.com"
2025-01-26 11:50:58 +08:00
spark
d3cf202c88 feat(frontend): 增加 AI 赋能股票分析功能
- 在 package.json 中添加 AI 赋能股票分析相关关键词
- 更新 settings.vue 中的 openAI 配置项,优化输入框类型和样式
- 在 wails.json 中添加 AI 赋能分析股票的说明
2025-01-26 11:44:50 +08:00
Lovegood
847cacc71e Update README.md 2025-01-24 10:58:10 +08:00
Lovegood
23149b8a28 Update README.md 2025-01-24 10:57:29 +08:00
Lovegood
698f496a3c Update README.md 2025-01-24 10:57:09 +08:00
Lovegood
8ac97f43ff Update README.md 2025-01-24 10:55:33 +08:00
Lovegood
0abf7c9e5a Update README.md 2025-01-24 10:54:00 +08:00
spark
a55920f445 feat(backend): 添加电报新闻功能
- 新增 GetTelegraphList 函数,用于获取电报新闻列表
- 在处理用户消息时,添加了获取电报新闻的协程
- 优化了消息处理流程,增加了电报新闻的回复
2025-01-23 17:29:18 +08:00
spark
775635a48c feat(backend): 添加通用聊天流功能并优化系统托盘事件处理
- 在 openai_api.go 中添加 NewCommonChatStream 函数,实现通用聊天流功能
- 修改 systray.Run 调用,使用 goroutine 异步执行 onReady 和 onExit 函数- 更新 stock.vue 中的 search函数,增加对多个股票信息页面的支持
2025-01-23 17:07:33 +08:00
spark
5bc7cfab0a feat(app): 更新股票信息显示和隐藏功能
- 在股票信息更新时,如果总价格不为0,设置系统托盘提示信息- 修复了显示和隐藏应用程序的功能
- 优化了股票数据 API 的请求 URL
- 替换 ioutil 包为 io 包,以适应 Go 1.16 及以上版本
2025-01-23 11:18:11 +08:00
spark
e3e06d342b feat(backend): 添加股票价格信息查询功能
- 新增 SearchStockPriceInfo 函数,用于查询股票价格信息
- 更新 NewChatStream 函数,增加股票代码参数- 在前端添加股票代码参数传递
- 优化后端接口测试用例
2025-01-22 17:00:14 +08:00
spark
16e187b96c docs: 更新 README 中的设置截图
- 将 README.md 中的 img.png 文件引用从根目录改为 build/screenshot 目录
- 更新设置截图,使用最新的 img_11.png 替换旧的 img.png
2025-01-22 15:41:47 +08:00
spark
399513cf14 feat(backend): 添加股票信息搜索功能并优化 OpenAI API调用
- 新增 SearchStockInfo 函数,用于搜索指定股票的相关信息
- 优化 OpenAI API 调用,使用搜索到的股票信息作为上下文- 更新 go.mod 和 go.sum 文件,添加 chromedp 等依赖
2025-01-22 15:15:41 +08:00
spark
3f024faf82 feat(frontend): 添加 AI 分析开关功能
- 在全局配置中获取 openAiEnable 状态
- 根据 openAiEnable 状态控制 AI 分析按钮的显示- 优化了组件的初始化逻辑,确保配置信息及时加载
2025-01-22 12:21:43 +08:00
spark
dadfe1cf54 feat(frontend): 实现 AI聊天流功能
- 新增 NewChatStream 函数,用于接收实时聊天流数据
- 在 App 组件中添加 NewChatStream 方法处理聊天流
- 修改前端 Stock 组件,支持实时显示 AI 聊天流结果
- 优化后端 OpenAi 结构,增加 NewChatStream 方法获取流式响应
2025-01-22 12:02:33 +08:00
spark
9cd6761778 feat(backend): 移除 OpenAI API 中的 Markdown 输出
- 删除了 OpenAI API 请求中的 Markdown 输出要求
- 注释掉了日志记录响应内容的代码行- 在 README 中添加了关于 AI 股票分析功能的重大更新说明
2025-01-17 15:33:04 +08:00
spark
1d58d6b224 feat(backend): 移除 OpenAI API 中的 Markdown 输出
- 删除了 OpenAI API 请求中的 Markdown 输出要求
- 注释掉了日志记录响应内容的代码行- 在 README 中添加了关于 AI 股票分析功能的重大更新说明
2025-01-17 15:29:57 +08:00
spark
db4a2b5fa9 feat(backend): 更新 OpenAI API 调用以支持 Markdown 输出
- 在请求内容中添加了对 Markdown 输出的要求
- 此更改将提高回复的可读性和格式化效果
2025-01-17 15:21:17 +08:00
spark
af3f2b03dc style(frontend): 优化 AI 分析结果弹窗样式
-调整弹窗高度为480px,增加垂直空间
- 设置 Markdown 预览区域高度为 380px,确保内容显示完整
-移除不必要的 previewTheme 属性,直接使用 theme 属性
2025-01-17 15:19:46 +08:00
spark
ccbb835c83 feat(frontend): 集成 OpenAI 聊天功能- 新增 NewChat 函数,用于与 OpenAI 进行聊天
- 在 App.d.ts 和 App.js 中添加 NewChat 方法的声明和实现
- 在 models.ts 中添加 OpenAI 相关的配置项
- 在 package.json 中添加 md-editor-v3 依赖,可能用于富文本编辑
2025-01-17 14:36:13 +08:00
spark
97b5faee4a refactor(app): 优化系统托盘相关代码
- 移除了 systray.Run 函数的冗余 goroutine
- 删除了多余的空行,提高了代码可读性
2025-01-15 13:01:12 +08:00
spark
c9a8192d60 style: 修改 .gitignore 文件中的路径格式
- 移除了 .gitignore 文件中路径开头的 ./
- 使路径格式更加规范和一致
2025-01-15 10:34:42 +08:00
spark
1af138312a chore: 更新 .gitignore 文件
- 移除对特定可执行文件的忽略规则
- 使用通配符忽略 build/bin 目录下的所有文件
- 删除 frontend/package.json.md5 忽略规则
2025-01-15 10:32:01 +08:00
spark
272c990248 build: 更新 .gitignore 文件- 添加 frontend/package.json.md5到忽略列表,避免无关文件影响版本控制 2025-01-15 10:29:31 +08:00
spark
2907285915 build: 更新 .gitignore 文件- 添加 frontend/package.json.md5到忽略列表,避免无关文件影响版本控制 2025-01-15 10:28:40 +08:00
sparkmemory
9f7b7b8a64 feat(app): 启动时添加股票价格监控
- 在 app.go 中添加了 MonitorStockPrices 函数的异步调用
- 修改了前端 App.vue 中的跑马灯效果,包括速度、样式和布局调整
- 更新了 package.json 的 MD5 哈希值
2025-01-14 23:42:43 +08:00
spark
02bfe4758e refactor(app): 调整系统托盘创建逻辑并更新应用配置
- 将系统托盘创建逻辑从 main.go 移动到 app.go 中的 startup 方法- 更新应用配置,添加生产环境日志级别配置
- 移除 main.go 中的冗余注释
2025-01-14 21:03:35 +08:00
spark
6483243d2a feat(stock): 添加电报资讯功能
- 在后端增加电报资讯抓取功能,定时刷新并发送到前端
- 在前端添加电报资讯显示组件,滚动显示最新资讯
- 更新 go.mod 和 go.sum 文件,添加相关依赖
2025-01-14 13:13:50 +08:00
spark
1ea534b3c0 refactor(app): 重构应用启动和托盘功能
- 移除 App.startup 中的系统托盘创建逻辑
- 在 main.go 中添加系统托盘创建逻辑- 更新前端 App.vue,添加实时盈亏显示和相关事件监听- 调整 stock.vue,引入通知功能
2025-01-14 11:31:15 +08:00
spark
2fcd89ab97 style(frontend): 优化页面元素的样式和布局
-调整了 App.vue 中底部菜单栏的 z-index 值- 在 settings.vue 中为数据刷新间隔输入框添加了单位提示- 优化了 stock.vue 中搜索框的布局和样式
2025-01-13 15:23:51 +08:00
spark
b5c44870fe docs: 更新 README 中的屏幕截图链接
- 更新了多个屏幕截图的文件名
- 调整了部分截图的展示顺序- 修正了一个截图链接的路径
2025-01-13 12:30:01 +08:00
spark
54f0a0b585 docs: 更新 README 中的屏幕截图链接
- 更新了多个屏幕截图的文件名
- 调整了部分截图的展示顺序- 修正了一个截图链接的路径
2025-01-13 12:18:27 +08:00
spark
ab6f400930 docs: 更新 README 中的屏幕截图链接
- 更新了多个屏幕截图的文件名
- 调整了部分截图的展示顺序- 修正了一个截图链接的路径
2025-01-13 12:18:08 +08:00
spark
a376d1d92c feat(settings): 添加基础设置功能- 在数据库中增加更新基础信息和刷新间隔的配置项
- 实现根据配置定时更新数据的功能
- 添加启动时更新基础信息的逻辑
- 更新前端设置界面,增加基础设置选项
2025-01-13 12:07:35 +08:00
spark
a653ef9fa8 feat(frontend): 优化 App.vue 中的全屏和隐藏功能- 为全屏和隐藏按钮添加了 title 属性,提升用户体验
- 优化了全屏切换逻辑,现在支持键盘快捷键 Ctrl+F 和 Esc
- 调整了全屏按钮的显示文本,根据当前状态动态变化
- 移除了不必要的 console.log 语句,简化了代码
2025-01-13 10:52:34 +08:00
sparkmemory
ce29514b54 feat(settings): 优化配置更新逻辑
- 增加对配置存在的检查,如果存在则更新,不存在则创建默认配置
- 添加日志记录,当配置不存在时创建默认配置的情况
- 通过 Where 子句指定 ID 进行更新,提高更新操作的准确性
2025-01-12 20:47:49 +08:00
sparkmemory
1fd149bbd5 feat(frontend): 添加窗口移动功能并优化错误处理
- 在 App.vue 中添加移动窗口功能
- 优化全屏切换逻辑
- 在 stock_data_api.go 中改进错误处理
- 移除 app.go 中的冗余日志
2025-01-11 22:54:07 +08:00
sparkmemory
9dc8fa97df feat(settings): 添加推送设置功能- 新增本地推送和钉钉推送的配置选项
- 实现配置的保存和读取功能- 添加测试通知按钮
-优化股票信息的显示格式
2025-01-11 14:16:28 +08:00
sparkmemory
7c52cd1d26 style(frontend): 优化页面布局和底部菜单样式
-调整 RouterView 的样式,增加底部填充
- 修改底部菜单的布局方式,从 sticky 改为 fixed,并设置宽度为 100%
- 更新 App.vue 文件中的相关代码
2025-01-10 20:24:12 +08:00
spark
338ce91ffd feat(frontend): 实现基本路由功能并添加设置页面
- 在 App.vue 中集成 vue-router
- 新增 router.js 文件配置路由- 添加设置页面组件和路由- 更新菜单选项,使用 RouterLink 替代普通链接
2025-01-10 18:23:12 +08:00
spark
6e5f57d62e feat(stock): 在股票卡片中添加取消关注按钮
- 在股票卡片的 header-extra 区域添加了一个取消关注按钮
- 按钮点击时调用 removeMonitor 方法,传入股票代码、名称和 key
- 移除了原有的可关闭图标
2025-01-10 15:46:43 +08:00
spark
b1a0e9575b feat(frontend): 优化用户界面和功能
- 添加全屏切换功能
- 实现窗口隐藏和退出功能
- 新增设置菜单
- 优化股票信息展示界面
- 调整窗口大小和布局
2025-01-10 15:32:22 +08:00
spark
88fb3ce94c refactor(linux): 移除未使用的 time 包导入
- 删除了 app_linux.go 文件中未使用的 time 包导入
-此修改提高了代码的整洁度和可维护性
2025-01-09 15:51:07 +08:00
spark
60d8efc158 refactor(linux): 移除未使用的 time 包导入
- 删除了 app_linux.go 文件中未使用的 time 包导入
-此修改提高了代码的整洁度和可维护性
2025-01-09 15:47:41 +08:00
spark
a41ab5499a refactor: 为 app_linux.go 添加 time 包导入
- 在 app_linux.go 文件中导入了 time 包
- 此修改可能为后续功能使用时间相关函数做准备
2025-01-09 15:42:47 +08:00
spark
a54f769ea2 feat(app_linux): 增加股票排序和消息发送功能
- 添加 SetStockSort 方法,用于设置股票排序
- 新增 SendDingDingMessageByType 方法,根据消息类型发送钉钉消息- 实现 GenNotificationMsg 方法,生成通知消息内容
- 添加 getMsgTypeTTL 和 getMsgTypeName 方法,用于获取消息类型的 TTL 和名称
- 优化 Greet 方法,处理返回的股票数据
2025-01-09 15:42:02 +08:00
spark
9a46788339 feat(app_linux): 增加股票排序和消息发送功能
- 添加 SetStockSort 方法,用于设置股票排序
- 新增 SendDingDingMessageByType 方法,根据消息类型发送钉钉消息- 实现 GenNotificationMsg 方法,生成通知消息内容
- 添加 getMsgTypeTTL 和 getMsgTypeName 方法,用于获取消息类型的 TTL 和名称
- 优化 Greet 方法,处理返回的股票数据
2025-01-09 15:37:20 +08:00
spark
def92ad722 fix(stock): 修正股票排序键的使用
- 将 result.Sort 修改为 result.sort,以匹配正确的属性名称
- 更新 GetSortKey 函数调用,使用正确的属性名称
2025-01-09 14:54:35 +08:00
spark
7e27996f17 feat(backend): 优化股票数据获取逻辑
- 修改 GetStockCodeRealTimeData 方法,支持批量获取多个股票代码的实时数据
- 新增 GetStockInfos 函数,用于获取关注股票的实时信息- 重构 getStockInfo 函数,提高代码复用性
- 优化数据处理逻辑,提高程序运行效率
2025-01-09 14:45:25 +08:00
spark
ad428f83f8 refactor(stock): 重构股票数据处理逻辑
- 移除定时更新标题的代码
- 优化股票数据获取和处理流程
- 增加更多股票相关信息的计算和展示
- 调整前端组件以适应新的数据结构
- 修复了一些潜在的数值计算问题
2025-01-09 13:31:18 +08:00
spark
d3c6c1d570 feat(app): 优化股票监控和交易时间判断
- 添加了判断是否为交易日和交易时间的函数
- 修改了股票价格更新逻辑,只在交易时间内进行监控
- 优化了股票价格显示,增加了上次当前价格字段
- 更新了前端组件,支持显示股票价格变化动画
2025-01-08 15:28:08 +08:00
spark
1554d3309d feat(backend): 实现股票价格实时监控功能
- 在 App 结构中添加定时更新股票价格的逻辑
- 实现 MonitorStockPrices 函数,用于更新关注股票的价格
- 在前端添加股票价格更新的事件处理
- 优化股票数据的获取和处理逻辑
2025-01-08 14:17:11 +08:00
spark
e7560f3e9b feat(backend): 添加 Windows 系统消息提醒功能
- 新增 AlertWindowsApi 结构体和 SendNotification 方法,用于发送 Windows 系统通知
- 实现 SendDingDingMessageByType 方法,支持根据不同消息类型发送通知
- 添加消息类型 TTL 和名称映射,优化消息发送逻辑
- 更新前端接口,增加 SendDingDingMessageByType 方法调用- 引入 go-toast 库,用于 Windows 系统通知
2025-01-08 10:57:17 +08:00
spark
daa29b37a5 feat(stock): 添加股票排序功能- 新增 SetStockSort 函数用于设置股票排序
- 在前端增加股票排序的输入和显示逻辑
- 修改后端数据库,增加股票排序字段
- 优化股票列表的渲染,支持按排序值进行排序
2025-01-07 13:29:16 +08:00
spark
9a41560bee refactor(frontend): 优化股票组件功能和布局
-调整了固定按钮的位置和样式
- 优化了股票搜索和添加功能的布局
- 移除了不必要的控制台日志输出- 调整了事件处理
2025-01-07 11:11:07 +08:00
spark
975ad611df build(frontend): 升级 naive-ui 至 2.41.0 版本
- 在 package.json 和 package-lock.json 中更新 naive-ui 版本
- 更新 package.json.md5 校验值
2025-01-07 09:54:37 +08:00
spark
b764770729 refactor(app): 重构应用控制逻辑并修正部分功能
-移除了 App.shutdown 方法中的 runtime.Quit 调用
- 将 runtime.Show 替换为 runtime.WindowShow
- 在全屏菜单项中移除了 MenuItem.Hide 调用
- 将 runtime.Hide替换为 runtime.WindowHide
2025-01-07 09:42:20 +08:00
spark
88aa793774 feat(app): 优化应用隐藏功能并添加错误日志
- 注释掉隐藏应用程序的代码,暂时禁用此功能
- 添加对话框错误日志记录,提高错误追踪能力
- 在 shutdown 函数中添加 runtime.Quit 调用,确保应用正确退出
-优化股票组件中的报警逻辑,增加对当前价格的判断
2025-01-07 09:27:24 +08:00
spark
180bec8866 docs: 更新 README.md 文件 2025-01-06 18:07:04 +08:00
spark
420d5b60f1 docs(README): 更新截图并添加钉钉报警通知说明
- 更新成本仓位设置截图
- 添加钉钉报警通知功能截图及说明
2025-01-06 17:59:06 +08:00
spark
af1bc685a7 build(ci): 更新 GitHub Actions 构建配置
- 为 Windows平台构建产物命名为 go-stock-windows-amd64.exe
-为 Linux 平台构建产物命名为 go-stock-linux-amd64- 通过明确指定构建输出名称,提高构建结果的可识别性和一致性
2025-01-06 17:33:54 +08:00
spark
b0922b0878 ci: 更新 GitHub Actions 构建矩阵
- 为 Windows 和 Linux 构建产物添加平台名称后缀
- 保持原有构建配置不变
2025-01-06 17:18:24 +08:00
spark
200a160acf refactor(app): 优化系统托盘和菜单相关代码
- 在 FileMenu 中添加了隐藏到托盘区的功能,仅在 Windows 平台上显示- 优化了代码结构,提高了可读性和可维护性
2025-01-06 16:56:32 +08:00
spark
9fae9fc034 feat: 添加 Linux 平台支持
- 新增 app_linux.go 文件,实现 Linux 平台下的应用逻辑
- 添加缓存功能,用于限制钉钉消息发送频率- 实现股票数据相关功能,包括获取股票列表、关注股票等
- 添加应用启动、关闭等生命周期方法
2025-01-06 16:44:22 +08:00
spark
9a3393bfc3 feat(app): 为 Windows 系统添加系统托盘功能并支持 Linux
- 在 app.go 中添加了对 Windows 操作系统的判断- 仅在 Windows 系统上创建系统托盘
- 更新 GitHub Actions 工作流,添加 Linux 平台的构建
2025-01-06 16:30:03 +08:00
spark
64270d5df2 ci: 更新 Windows 构建输出文件名
- 将 Windows构建输出文件名从 'go-stock' 改为 'go-stock.exe'
- 确保在 Windows 平台上生成可执行文件
2025-01-06 15:19:32 +08:00
spark
e808ca47b6 refactor(app): 注释掉退出相关的代码
- 在 App.d.ts 中注释掉了 systray.Quit() 调用
- 在 App.js 中注释掉了 FileMenu 中的退出选项
- 更新了 models.ts 的 MD5 校验值
2025-01-06 15:18:33 +08:00
sparkmemory
1b3c043ce6 feat(stock): 增加股价提醒功能并优化报警逻辑
- 在 SetAlarmChangePercent 函数中添加 alarmPrice 参数
- 在前端添加股价提醒输入框
- 修改报警逻辑,支持同时根据涨跌幅和股价进行提醒
- 更新数据库模型,添加 AlarmPrice 字段
2025-01-04 20:54:04 +08:00
39 changed files with 3060 additions and 327 deletions

View File

@@ -17,9 +17,12 @@ jobs:
fail-fast: false
matrix:
build:
- name: 'go-stock'
- name: 'go-stock-windows-amd64.exe'
platform: 'windows/amd64'
os: 'windows-latest'
# - name: 'go-stock-linux-amd64'
# platform: 'linux/amd64'
# os: 'ubuntu-latest'
runs-on: ${{ matrix.build.os }}
steps:
@@ -29,7 +32,7 @@ jobs:
submodules: recursive
- name: Build wails
uses: dAppServer/wails-build-action@v2.2
uses: ArvinLovegood/wails-build-action@v2.3
id: build
with:
build-name: ${{ matrix.build.name }}

8
.gitignore vendored
View File

@@ -106,7 +106,7 @@ dist
.DS_Store
.idea/
data/*.db
./build/*.exe
./build/bin/go-stock-dev.exe
./build/bin/go-stock.exe
/data/*.db
/build/*.exe
/build/bin/*
frontend/package.json.md5

View File

@@ -1,21 +1,33 @@
# README
![Wails and NaiveUI](./build/appicon.png)
## 自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具
## BIG NEWS !!! 重大更新!!!
- 2025.01.17 新增AI大模型分析股票功能
![img_5.png](build/screenshot/img_10.png)
## 简介
- 本项目基于Wails和NaiveUI纯属无聊仅供娱乐不喜勿喷。
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
## Snapshot
![img_1.png](build/screenshot/img_1.png)
### 成本仓位设置
![img.png](build/screenshot/img.png)
![img_1.png](build/screenshot/img_6.png)
### 设置
![img.png](build/screenshot/img_11.png)
### 成本设置
![img.png](build/screenshot/img_7.png)
### 日K
![img_2.png](build/screenshot/img_2.png)
![img_2.png](build/screenshot/img_8.png)
### 分时
![img_3.png](build/screenshot/img_3.png)
![img_3.png](build/screenshot/img_9.png)
### 钉钉报警通知
![img_4.png](build/screenshot/img_5.png)
### AI分析股票
![img_5.png](build/screenshot/img_10.png)
## About
A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
### A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
A股数据可视化工具基于Wails和NaiveUI。
## Prerequisites
@@ -38,4 +50,4 @@ You can build you Application with: `wails build`
[NaiveUI](https://www.naiveui.com/)
[Wails](https://wails.io/)
[Vue](https://vuejs.org/)
[Vite](https://vitejs.dev/)
[Vite](https://vitejs.dev/)

353
app.go
View File

@@ -1,12 +1,23 @@
//go:build windows
package main
import (
"context"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice"
"github.com/getlantern/systray"
"github.com/go-resty/resty/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"strings"
"time"
)
// App struct
@@ -30,10 +41,10 @@ func (a *App) startup(ctx context.Context) {
a.ctx = ctx
// 创建系统托盘
go systray.Run(func() {
onReady(a)
systray.Run(func() {
go onReady(a)
}, func() {
onExit(a)
go onExit(a)
})
}
@@ -41,14 +52,226 @@ func (a *App) startup(ctx context.Context) {
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
//ticker := time.NewTicker(time.Second)
//defer ticker.Stop()
////定时更新数据
//go func() {
// for range ticker.C {
// runtime.WindowSetTitle(ctx, "go-stock "+time.Now().Format("2006-01-02 15:04:05"))
//定时更新数据
go func() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
interval := config.RefreshInterval
if interval <= 0 {
interval = 1
}
ticker := time.NewTicker(time.Second * time.Duration(interval))
defer ticker.Stop()
for range ticker.C {
if isTradingTime(time.Now()) {
MonitorStockPrices(a)
}
}
}()
go func() {
ticker := time.NewTicker(time.Second * time.Duration(60))
defer ticker.Stop()
for range ticker.C {
telegraph := refreshTelegraphList()
if telegraph != nil {
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
}
}
}()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
go MonitorStockPrices(a)
}
func refreshTelegraphList() *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, selection.Text())
})
return &telegraph
}
// isTradingDay 判断是否是交易日
func isTradingDay(date time.Time) bool {
weekday := date.Weekday()
// 判断是否是周末
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
// 这里可以添加具体的节假日判断逻辑
// 例如:判断是否是春节、国庆节等
return true
}
// isTradingTime 判断是否是交易时间
func isTradingTime(date time.Time) bool {
if !isTradingDay(date) {
return false
}
hour, minute, _ := date.Clock()
// 判断是否在9:15到11:30之间
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
return true
}
// 判断是否在13:00到15:00之间
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
return true
}
return false
}
func MonitorStockPrices(a *App) {
dest := &[]data.FollowedStock{}
db.Dao.Model(&data.FollowedStock{}).Find(dest)
total := float64(0)
//for _, follow := range *dest {
// stockData := getStockInfo(follow)
// total += stockData.ProfitAmountToday
// price, _ := convertor.ToFloat(stockData.Price)
// if stockData.PrePrice != price {
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
// }
//}()
//}
stockInfos := GetStockInfos(*dest...)
for _, stockInfo := range *stockInfos {
total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price {
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
}
}
if total != 0 {
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
systray.SetTooltip(title)
}
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
//runtime.WindowSetTitle(a.ctx, title)
}
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockCodes := make([]string, 0)
for _, follow := range follows {
stockCodes = append(stockCodes, follow.StockCode)
}
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
if err != nil {
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
return nil
}
stockInfos := make([]data.StockInfo, 0)
for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
return follow.StockCode == info.Code
})
if ok {
addStockFollowData(v, &info)
stockInfos = append(stockInfos, info)
}
}
return &stockInfos
}
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
stockCode := follow.StockCode
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
if err != nil || len(*stockDatas) == 0 {
return &data.StockInfo{}
}
stockData := (*stockDatas)[0]
addStockFollowData(follow, &stockData)
return &stockData
}
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.PrePrice = follow.Price //上次当前价格
stockData.Sort = follow.Sort
stockData.CostPrice = follow.CostPrice //成本价
stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice
//当前价格
price, _ := convertor.ToFloat(stockData.Price)
//当前价格为0 时 使用卖一价格作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.A1P)
}
//当前价格依然为0 时 使用买一报价作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.B1P)
}
//昨日收盘价
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
//当前价格依然为0 时 使用昨日收盘价为当前价格
if price == 0 {
price = preClosePrice
}
//今日最高价
highPrice, _ := convertor.ToFloat(stockData.High)
if highPrice == 0 {
highPrice, _ = convertor.ToFloat(stockData.Open)
}
//今日最低价
lowPrice, _ := convertor.ToFloat(stockData.Low)
if lowPrice == 0 {
lowPrice, _ = convertor.ToFloat(stockData.Open)
}
//开盘价
//openPrice, _ := convertor.ToFloat(stockData.Open)
if price > 0 {
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
}
if highPrice > 0 {
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
}
if lowPrice > 0 {
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
}
if follow.CostPrice > 0 && follow.Volume > 0 {
if price > 0 {
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
} else {
//未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
}
}
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
if follow.Price != price && price > 0 {
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
"price": price,
})
}
}
// beforeClose is called when the application is about to quit,
@@ -66,6 +289,7 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
})
if err != nil {
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
@@ -82,8 +306,14 @@ func (a *App) shutdown(ctx context.Context) {
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) *data.StockInfo {
stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(name)
func (a *App) Greet(stockCode string) *data.StockInfo {
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
follow := &data.FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
stockInfo := getStockInfo(*follow)
return stockInfo
}
@@ -107,10 +337,12 @@ func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int6
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, stockCode)
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
@@ -125,6 +357,80 @@ func (a *App) SendDingDingMessage(message string, stockCode string) string {
return data.NewDingDingAPI().SendDingDingMessage(message)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
stockInfo := &data.StockInfo{}
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChat(stock string) string {
return data.NewDeepSeekOpenAi().NewChat(stock)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func onExit(a *App) {
// 清理操作
logger.SugaredLogger.Infof("onExit")
@@ -132,12 +438,12 @@ func onExit(a *App) {
}
func onReady(a *App) {
// 初始化操作
logger.SugaredLogger.Infof("onReady")
systray.SetIcon(icon2)
systray.SetTitle("go-stock")
systray.SetTooltip("go-stock 股票行情实时获取")
// 创建菜单项
show := systray.AddMenuItem("显示", "显示应用程序")
hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
@@ -154,13 +460,22 @@ func onReady(a *App) {
//systray.Quit()
case <-show.ClickedCh:
logger.SugaredLogger.Infof("显示应用程序")
runtime.Show(a.ctx)
//runtime.WindowShow(a.ctx)
runtime.WindowShow(a.ctx)
//runtime.WindowShow(a.ctx)
case <-hide.ClickedCh:
logger.SugaredLogger.Infof("隐藏应用程序")
runtime.Hide(a.ctx)
runtime.WindowHide(a.ctx)
}
}
}()
}
func (a *App) UpdateConfig(settings *data.Settings) string {
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

193
app_linux.go Normal file
View File

@@ -0,0 +1,193 @@
//go:build linux
// +build linux
package main
import (
"context"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/logger"
)
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
return &App{
cache: cache,
}
}
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
// Perform your setup here
a.ctx = ctx
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
//ticker := time.NewTicker(time.Second)
//defer ticker.Stop()
////定时更新数据
//go func() {
// for range ticker.C {
// runtime.WindowSetTitle(ctx, "go-stock "+time.Now().Format("2006-01-02 15:04:05"))
// }
//}()
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
if err != nil {
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
}
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) *data.StockInfo {
stockDatas, _ := data.NewStockDataApi().GetStockCodeRealTimeData(name)
stockData := (*stockDatas)[0]
return &stockData
}
func (a *App) Follow(stockCode string) string {
return data.NewStockDataApi().Follow(stockCode)
}
func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode)
}
func (a *App) GetFollowList() []data.FollowedStock {
return data.NewStockDataApi().GetFollowList()
}
func (a *App) GetStockList(key string) []data.StockBasic {
return data.NewStockDataApi().GetStockList(key)
}
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func (a *App) UpdateConfig(settings *data.Settings) string {
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

View File

@@ -0,0 +1,53 @@
//go:build windows
package data
import (
"github.com/go-toast/toast"
"go-stock/backend/logger"
)
// AlertWindowsApi @Author spark
// @Date 2025/1/8 9:40
// @Desc
// -----------------------------------------------------------------------------------
type AlertWindowsApi struct {
AppID string
// 窗口标题
Title string
// 窗口内容
Content string
// 窗口图标
Icon string
}
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
return &AlertWindowsApi{
AppID: AppID,
Title: Title,
Content: Content,
Icon: Icon,
}
}
func (a AlertWindowsApi) SendNotification() bool {
if getConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}
notification := toast.Notification{
AppID: a.AppID,
Title: a.Title,
Message: a.Content,
Icon: a.Icon,
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return false
}
return true
}

View File

@@ -0,0 +1,30 @@
//go:build windows
package data
import (
"github.com/go-toast/toast"
"go-stock/backend/logger"
"testing"
)
// @Author spark
// @Date 2025/1/8 9:40
// @Desc
//-----------------------------------------------------------------------------------
func TestAlert(t *testing.T) {
notification := toast.Notification{
AppID: "go-stock",
Title: "Hello, World!",
Message: "This is a toast notification.",
Icon: "../../build/appicon.png",
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return
}
}

View File

@@ -10,8 +10,6 @@ import (
// @Desc
//-----------------------------------------------------------------------------------
const dingding_robot_url = "https://oapi.dingtalk.com/robot/send?access_token=0237527b404598f37ae5d83ef36e936860c7ba5d3892cd43f64c4159d3ed7cb1"
type DingDingAPI struct {
client *resty.Client
}
@@ -23,11 +21,15 @@ func NewDingDingAPI() *DingDingAPI {
}
func (DingDingAPI) SendDingDingMessage(message string) string {
if getConfig().DingPushEnable == false {
logger.SugaredLogger.Info("钉钉推送未开启")
return "钉钉推送未开启"
}
// 发送钉钉消息
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(message).
Post(dingding_robot_url)
Post(getApiURL())
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "发送钉钉消息失败"
@@ -35,6 +37,12 @@ func (DingDingAPI) SendDingDingMessage(message string) string {
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
return "发送钉钉消息成功"
}
func getConfig() *Settings {
return NewSettingsApi(&Settings{}).GetConfig()
}
func getApiURL() string {
return getConfig().DingRobot
}
func (DingDingAPI) SendToDingDing(title, message string) string {
// 发送钉钉消息
@@ -50,7 +58,7 @@ func (DingDingAPI) SendToDingDing(title, message string) string {
IsAtAll: true,
},
}).
Post(dingding_robot_url)
Post(getApiURL())
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "发送钉钉消息失败"

View File

@@ -11,6 +11,7 @@ import (
//-----------------------------------------------------------------------------------
func TestRobot(t *testing.T) {
dingdingRobotUrl := "XXX"
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(`{
@@ -23,7 +24,7 @@ func TestRobot(t *testing.T) {
"isAtAll": true
}
}`).
Post(dingding_robot_url)
Post(dingdingRobotUrl)
if err != nil {
t.Error(err)
}

376
backend/data/openai_api.go Normal file
View File

@@ -0,0 +1,376 @@
package data
import (
"bufio"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
"strings"
"sync"
"time"
)
// @Author spark
// @Date 2025/1/16 13:19
// @Desc
// -----------------------------------------------------------------------------------
type OpenAi struct {
BaseUrl string `json:"base_url"`
ApiKey string `json:"api_key"`
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature"`
Prompt string `json:"prompt"`
}
func NewDeepSeekOpenAi() *OpenAi {
config := getConfig()
return &OpenAi{
BaseUrl: config.OpenAiBaseUrl,
ApiKey: config.OpenAiApiKey,
Model: config.OpenAiModelName,
MaxTokens: config.OpenAiMaxTokens,
Temperature: config.OpenAiTemperature,
Prompt: config.Prompt,
}
}
type THSTokenResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data"`
}
type AiResponse struct {
Id string `json:"id"`
Object string `json:"object"`
Created int `json:"created"`
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
Logprobs interface{} `json:"logprobs"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
PromptCacheHitTokens int `json:"prompt_cache_hit_tokens"`
PromptCacheMissTokens int `json:"prompt_cache_miss_tokens"`
} `json:"usage"`
SystemFingerprint string `json:"system_fingerprint"`
}
func (o OpenAi) NewChat(stock string) string {
client := resty.New()
client.SetBaseURL(o.BaseUrl)
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
res := &AiResponse{}
_, err := client.R().
SetResult(res).
SetBody(map[string]interface{}{
"model": o.Model,
"max_tokens": o.MaxTokens,
"temperature": o.Temperature,
"messages": []map[string]interface{}{
{
"role": "system",
"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:" +
"1. 市场背景:\n" +
"- 当前A股市场整体走势(如:牛市、熊市、震荡市)\n " +
"- 近期影响市场的主要宏观经济因素\n " +
"- 市场情绪指标(如:融资融券余额、成交量变化) " +
"2. 技术指标分析: " +
"- 当前股价水平" +
"- 所在boll区间" +
"- 上证指数的MA(移动平均线)、MACD、KDJ指标分析\n " +
"- 行业板块轮动情况\n " +
"- 近期市场热点和龙头股票的技术形态 " +
"3. 风险评估:\n " +
"- 当前市场主要风险因素\n " +
"- 如何设置止损和止盈位\n " +
"- 资金管理建议(如:仓位控制) " +
"4. 投资策略:\n " +
"- 短期(1-2周)、中期(1-3月)和长期(3-6月)的市场预期\n " +
"- 不同风险偏好投资者的策略建议\n " +
"- 值得关注的行业板块和个股推荐(请给出2-3个具体例子,包括股票代码和名称) " +
"5. 技术面和基本面结合:\n " +
"- 如何将技术分析与公司基本面分析相结合\n " +
"- 识别高质量股票的关键指标 " +
"请提供详细的分析和具体的操作建议,包括入场时机、持仓周期和退出策略。同时,请强调风险控制的重要性,并提醒投资者需要根据自身情况做出决策。 " +
"你的分析和建议应当客观、全面,并基于当前可获得的市场数据。如果某些信息无法确定,请明确指出并解释原因。",
},
{
"role": "user",
"content": "点评一下" + stock,
},
},
}).
Post("/chat/completions")
if err != nil {
return ""
}
//logger.SugaredLogger.Infof("%v", res.Choices[0].Message.Content)
return res.Choices[0].Message.Content
}
func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string {
ch := make(chan string)
go func() {
defer close(ch)
client := resty.New()
client.SetBaseURL(o.BaseUrl)
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3)
client.SetTimeout(time.Second * 30)
msg := []map[string]interface{}{
{
"role": "system",
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合注重风险控制。\n\n【核心功能】\n\n市场分析维度\n\n宏观经济GDP/CPI/货币政策)\n\n行业景气度产业链/政策红利/技术革新)\n\n个股三维诊断\n\n基本面PE/PB/ROE/现金流/护城河\n\n技术面K线形态/均线系统/量价关系/指标背离\n\n资金面主力动向/北向资金/融资余额/大宗交易\n\n智能策略库\n√ 趋势跟踪策略(鳄鱼线+ADX\n√ 波段交易策略(斐波那契回撤+RSI\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系\n▶ 动态止损ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议附压力测试结果\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑3点关键驱动力\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时采用苏格拉底式追问\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式\n📈 标的名称XXXXXX\n⚖ 多空信号:金叉确认/顶背离预警\n🎯 关键价位支撑位XX.XX/压力位XX.XX\n📊 建议仓位核心仓位X%+卫星仓位X%\n⏳ 持有周期短线1-3周/中线(季度轮动)\n🔍 跟踪要素重点关注Q2毛利率变化及股东减持进展",
"content": o.Prompt,
},
}
wg := &sync.WaitGroup{}
wg.Add(4)
go func() {
defer wg.Done()
messages := SearchStockPriceInfo(stockCode)
price := ""
for _, message := range *messages {
price += message + ";"
}
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + "当前价格:" + price,
})
}()
go func() {
defer wg.Done()
messages := GetTelegraphList()
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
}
}()
go func() {
defer wg.Done()
messages := SearchStockInfo(stock, "depth")
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
}
}()
go func() {
defer wg.Done()
messages := SearchStockInfo(stock, "telegram")
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
}
}()
wg.Wait()
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "分析和总结",
})
resp, err := client.R().
SetDoNotParseResponse(true).
SetBody(map[string]interface{}{
"model": o.Model,
"max_tokens": o.MaxTokens,
"temperature": o.Temperature,
"stream": true,
"messages": msg,
}).
Post("/chat/completions")
if err != nil {
ch <- err.Error()
return
}
defer resp.RawBody().Close()
scanner := bufio.NewScanner(resp.RawBody())
for scanner.Scan() {
line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "chat data: ") {
data := strings.TrimPrefix(line, "data: ")
if data == "[DONE]" {
return
}
var streamResponse struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
} `json:"delta"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
for _, choice := range streamResponse.Choices {
if content := choice.Delta.Content; content != "" {
ch <- content
}
}
}
}
}
}()
return ch
}
func (o OpenAi) NewCommonChatStream(stock, stockCode, apiURL, apiKey, Model string) <-chan string {
ch := make(chan string)
go func() {
defer close(ch)
client := resty.New()
client.SetHeader("Authorization", "Bearer "+apiKey)
client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3)
msg := []map[string]interface{}{
{
"role": "system",
"content": o.Prompt,
},
}
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
messages := SearchStockPriceInfo(stockCode)
price := ""
for _, message := range *messages {
price += message + ";"
}
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + "当前价格:" + price,
})
}()
//go func() {
// defer wg.Done()
// messages := SearchStockInfo(stock, "depth")
// for _, message := range *messages {
// msg = append(msg, map[string]interface{}{
// "role": "assistant",
// "content": message,
// })
// }
//}()
//go func() {
// defer wg.Done()
// messages := SearchStockInfo(stock, "telegram")
// for _, message := range *messages {
// msg = append(msg, map[string]interface{}{
// "role": "assistant",
// "content": message,
// })
// }
//}()
wg.Wait()
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "分析和总结",
})
resp, err := client.R().
SetDoNotParseResponse(true).
SetBody(map[string]interface{}{
"model": Model,
"max_tokens": o.MaxTokens,
"temperature": o.Temperature,
"stream": true,
"messages": msg,
}).
Post(apiURL)
if err != nil {
ch <- err.Error()
return
}
defer resp.RawBody().Close()
scanner := bufio.NewScanner(resp.RawBody())
for scanner.Scan() {
line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "data:") {
data := strings.TrimPrefix(line, "data:")
if data == "[DONE]" {
return
}
var streamResponse struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
} `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
for _, choice := range streamResponse.Choices {
if content := choice.Delta.Content; content != "" {
ch <- content
}
if choice.FinishReason == "stop" {
return
}
}
}
}
}
}()
return ch
}
func GetTelegraphList() *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, selection.Text())
})
return &telegraph
}

View File

@@ -0,0 +1,21 @@
package data
import (
"go-stock/backend/db"
"testing"
)
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db")
ai := NewDeepSeekOpenAi()
res := ai.NewChatStream("闻泰科技", "sh600745")
for {
select {
case msg := <-res:
if msg == "" {
return
}
t.Log(msg)
}
}
}

View File

@@ -0,0 +1,84 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/logger"
"gorm.io/gorm"
)
type Settings struct {
gorm.Model
TushareToken string `json:"tushareToken"`
LocalPushEnable bool `json:"localPushEnable"`
DingPushEnable bool `json:"dingPushEnable"`
DingRobot string `json:"dingRobot"`
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
RefreshInterval int64 `json:"refreshInterval"`
OpenAiEnable bool `json:"openAiEnable"`
OpenAiBaseUrl string `json:"openAiBaseUrl"`
OpenAiApiKey string `json:"openAiApiKey"`
OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"`
Prompt string `json:"prompt"`
}
func (receiver Settings) TableName() string {
return "settings"
}
type SettingsApi struct {
Config Settings
}
func NewSettingsApi(settings *Settings) *SettingsApi {
return &SettingsApi{
Config: *settings,
}
}
func (s SettingsApi) UpdateConfig() string {
count := int64(0)
db.Dao.Model(s.Config).Count(&count)
if count > 0 {
db.Dao.Model(s.Config).Where("id=?", s.Config.ID).Updates(map[string]any{
"local_push_enable": s.Config.LocalPushEnable,
"ding_push_enable": s.Config.DingPushEnable,
"ding_robot": s.Config.DingRobot,
"update_basic_info_on_start": s.Config.UpdateBasicInfoOnStart,
"refresh_interval": s.Config.RefreshInterval,
"open_ai_enable": s.Config.OpenAiEnable,
"open_ai_base_url": s.Config.OpenAiBaseUrl,
"open_ai_api_key": s.Config.OpenAiApiKey,
"open_ai_model_name": s.Config.OpenAiModelName,
"open_ai_max_tokens": s.Config.OpenAiMaxTokens,
"open_ai_temperature": s.Config.OpenAiTemperature,
"tushare_token": s.Config.TushareToken,
"prompt": s.Config.Prompt,
})
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
db.Dao.Model(s.Config).Create(&Settings{
LocalPushEnable: s.Config.LocalPushEnable,
DingPushEnable: s.Config.DingPushEnable,
DingRobot: s.Config.DingRobot,
UpdateBasicInfoOnStart: s.Config.UpdateBasicInfoOnStart,
RefreshInterval: s.Config.RefreshInterval,
OpenAiEnable: s.Config.OpenAiEnable,
OpenAiBaseUrl: s.Config.OpenAiBaseUrl,
OpenAiApiKey: s.Config.OpenAiApiKey,
OpenAiModelName: s.Config.OpenAiModelName,
OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
OpenAiTemperature: s.Config.OpenAiTemperature,
TushareToken: s.Config.TushareToken,
Prompt: s.Config.Prompt,
})
}
return "保存成功!"
}
func (s SettingsApi) GetConfig() *Settings {
var settings Settings
db.Dao.Model(&Settings{}).First(&settings)
return &settings
}

View File

@@ -6,11 +6,15 @@ package data
//-----------------------------------------------------------------------------------
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
"github.com/duke-git/lancet/v2/validator"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
@@ -18,54 +22,69 @@ import (
"golang.org/x/text/transform"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"io/ioutil"
"io"
"strings"
"time"
)
// http://hq.sinajs.cn/rn=1730966120830&list=sh600000,sh600859
const sina_stook_url = "http://hq.sinajs.cn/rn=%d&list=%s"
const tushare_api_url = "http://api.tushare.pro"
const TushareToken = "9125ec636217a99a3218a64fc63507e95205f2666590792923cbaedf"
const sinaStockUrl = "http://hq.sinajs.cn/rn=%d&list=%s"
const tushareApiUrl = "http://api.tushare.pro"
type StockDataApi struct {
client *resty.Client
config *Settings
}
type StockInfo struct {
gorm.Model
Date string `json:"日期" gorm:"index"`
Time string `json:"时间" gorm:"index"`
Code string `json:"股票代码" gorm:"index"`
Name string `json:"股票名称" gorm:"index"`
Price string `json:"当前价格"`
Volume string `json:"成交的股票数"`
Amount string `json:"成交金额"`
Open string `json:"今日开盘价"`
PreClose string `json:"昨日收盘价"`
High string `json:"今日最低价"`
Low string `json:"今日最高价"`
Bid string `json:"竞买价"`
Ask string `json:"竞价"`
B1P string `json:"买一报价"`
B1V string `json:"买一报"`
B2P string `json:"买二报价"`
B2V string `json:"买二报"`
B3P string `json:"买三报价"`
B3V string `json:"买三报"`
B4P string `json:"买四报价"`
B4V string `json:"买四报"`
B5P string `json:"买五报价"`
B5V string `json:"买五报"`
A1P string `json:"卖一报价"`
A1V string `json:"卖一报"`
A2P string `json:"卖二报价"`
A2V string `json:"卖二报"`
A3P string `json:"卖三报价"`
A3V string `json:"卖三报"`
A4P string `json:"卖四报价"`
A4V string `json:"卖四报"`
A5P string `json:"卖五报价"`
A5V string `json:"卖五报"`
Date string `json:"日期" gorm:"index"`
Time string `json:"时间" gorm:"index"`
Code string `json:"股票代码" gorm:"index"`
Name string `json:"股票名称" gorm:"index"`
PrePrice float64 `json:"上次当前价格"`
Price string `json:"当前价格"`
Volume string `json:"成交的股票数"`
Amount string `json:"成交金额"`
Open string `json:"今日开盘价"`
PreClose string `json:"昨日收盘价"`
High string `json:"今日最高价"`
Low string `json:"今日最低价"`
Bid string `json:"竞价"`
Ask string `json:"竞卖价"`
B1P string `json:"买一报"`
B1V string `json:"买一申报"`
B2P string `json:"买二报"`
B2V string `json:"买二申报"`
B3P string `json:"买三报"`
B3V string `json:"买三申报"`
B4P string `json:"买四报"`
B4V string `json:"买四申报"`
B5P string `json:"买五报"`
B5V string `json:"买五申报"`
A1P string `json:"卖一报"`
A1V string `json:"卖一申报"`
A2P string `json:"卖二报"`
A2V string `json:"卖二申报"`
A3P string `json:"卖三报"`
A3V string `json:"卖三申报"`
A4P string `json:"卖四报"`
A4V string `json:"卖四申报"`
A5P string `json:"卖五报"`
A5V string `json:"卖五申报"`
//以下是字段值需二次计算
ChangePercent float64 `json:"changePercent"` //涨跌幅
ChangePrice float64 `json:"changePrice"` //涨跌额
HighRate float64 `json:"highRate"` //最高涨跌
LowRate float64 `json:"lowRate"` //最低涨跌
CostPrice float64 `json:"costPrice"` //成本价
CostVolume int64 `json:"costVolume"` //持仓数量
Profit float64 `json:"profit"` //总盈亏率
ProfitAmount float64 `json:"profitAmount"` //总盈亏金额
ProfitAmountToday float64 `json:"profitAmountToday"` //今日盈亏金额
Sort int64 `json:"sort"` //排序
AlarmChangePercent float64 `json:"alarmChangePercent"`
AlarmPrice float64 `json:"alarmPrice"`
}
func (receiver StockInfo) TableName() string {
@@ -135,6 +154,7 @@ type FollowedStock struct {
PriceChange float64
ChangePercent float64
AlarmChangePercent float64
AlarmPrice float64
Time time.Time
Sort int64
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
@@ -162,6 +182,7 @@ func (receiver StockBasic) TableName() string {
func NewStockDataApi() *StockDataApi {
return &StockDataApi{
client: resty.New(),
config: getConfig(),
}
}
@@ -173,11 +194,11 @@ func (receiver StockDataApi) GetIndexBasic() {
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: "index_basic",
Token: TushareToken,
Token: receiver.config.TushareToken,
Params: nil,
Fields: fields}).
SetResult(res).
Post(tushare_api_url)
Post(tushareApiUrl)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
@@ -217,12 +238,12 @@ func (receiver StockDataApi) GetStockBaseInfo() {
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: "stock_basic",
Token: TushareToken,
Token: receiver.config.TushareToken,
Params: nil,
Fields: fields,
}).
SetResult(res).
Post(tushare_api_url)
Post(tushareApiUrl)
//logger.SugaredLogger.Infof("GetStockBaseInfo %s", string(resp.Body()))
//resp.Body()写入文件
//ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
@@ -256,42 +277,63 @@ func (receiver StockDataApi) GetStockBaseInfo() {
}
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCode string) (*StockInfo, error) {
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]StockInfo, error) {
resp, err := receiver.client.R().
SetHeader("Host", "hq.sinajs.cn").
SetHeader("Referer", "https://finance.sina.com.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
Get(fmt.Sprintf(sina_stook_url, time.Now().Unix(), StockCode))
Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), slice.Join(StockCodes, ",")))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &StockInfo{}, nil
return &[]StockInfo{}, err
}
stockData, err := ParseFullSingleStockData(GB18030ToUTF8(resp.Body()))
var count int64
db.Dao.Model(&StockInfo{}).Where("code = ?", StockCode).Count(&count)
if count == 0 {
go db.Dao.Model(&StockInfo{}).Create(stockData)
} else {
go db.Dao.Model(&StockInfo{}).Where("code = ?", StockCode).Updates(stockData)
stockInfos := make([]StockInfo, 0)
str := GB18030ToUTF8(resp.Body())
dataStr := strutil.SplitEx(str, "\n", true)
for _, data := range dataStr {
//logger.SugaredLogger.Info(data)
stockData, err := ParseFullSingleStockData(data)
if err != nil {
logger.SugaredLogger.Error(err.Error())
continue
}
stockInfos = append(stockInfos, *stockData)
go func() {
var count int64
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Count(&count)
if count == 0 {
db.Dao.Model(&StockInfo{}).Create(stockData)
} else {
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Updates(stockData)
}
}()
}
return stockData, err
return &stockInfos, err
}
func (receiver StockDataApi) Follow(stockCode string) string {
logger.SugaredLogger.Infof("Follow %s", stockCode)
stockInfo, err := receiver.GetStockCodeRealTimeData(stockCode)
stockInfos, err := receiver.GetStockCodeRealTimeData(stockCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "关注失败"
}
stockInfo := (*stockInfos)[0]
price, _ := convertor.ToFloat(stockInfo.Price)
db.Dao.Model(&FollowedStock{}).FirstOrCreate(&FollowedStock{
StockCode: stockCode,
Name: stockInfo.Name,
Price: price,
Time: time.Now(),
ChangePercent: 0,
PriceChange: 0,
StockCode: stockCode,
Name: stockInfo.Name,
Price: price,
Time: time.Now(),
ChangePercent: 0,
PriceChange: 0,
Sort: 0,
AlarmChangePercent: 3,
AlarmPrice: price + 1,
}, &FollowedStock{StockCode: stockCode})
return "关注成功"
}
@@ -310,8 +352,11 @@ func (receiver StockDataApi) SetCostPriceAndVolume(price float64, volume int64,
return "设置成功"
}
func (receiver StockDataApi) SetAlarmChangePercent(val float64, stockCode string) string {
err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", stockCode).Update("alarm_change_percent", val).Error
func (receiver StockDataApi) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", stockCode).Updates(&map[string]any{
"alarm_change_percent": val,
"alarm_price": alarmPrice,
}).Error
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "设置失败"
@@ -319,6 +364,10 @@ func (receiver StockDataApi) SetAlarmChangePercent(val float64, stockCode string
return "设置成功"
}
func (receiver StockDataApi) SetStockSort(sort int64, stockCode string) {
db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", stockCode).Update("sort", sort)
}
func (receiver StockDataApi) GetFollowList() []FollowedStock {
var result []FollowedStock
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)
@@ -345,10 +394,10 @@ func (receiver StockDataApi) GetStockList(key string) []StockBasic {
return result
}
// GB18030 转换为 UTF8
// GB18030ToUTF8 GB18030 转换为 UTF8
func GB18030ToUTF8(bs []byte) string {
reader := transform.NewReader(bytes.NewReader(bs), simplifiedchinese.GB18030.NewDecoder())
d, err := ioutil.ReadAll(reader)
d, err := io.ReadAll(reader)
if err != nil {
panic(err)
}
@@ -461,3 +510,86 @@ type IndexBasic struct {
func (IndexBasic) TableName() string {
return "tushare_index_basic"
}
func SearchStockPriceInfo(stockCode string) *[]string {
url := "https://www.cls.cn/stock?code=" + stockCode
// 创建一个 chromedp 上下文
ctx, cancel := chromedp.NewContext(
context.Background(),
)
defer cancel()
var htmlContent string
var tasks chromedp.Tasks
tasks = append(tasks, chromedp.Navigate(url))
tasks = append(tasks, chromedp.WaitVisible("div.quote-change-box", chromedp.ByQuery))
tasks = append(tasks, chromedp.ActionFunc(func(ctx context.Context) error {
chromedp.WaitVisible("span.quote-price", chromedp.ByQuery)
price := ""
for {
chromedp.Text("span.quote-price", &price, chromedp.BySearch).Do(ctx)
logger.SugaredLogger.Infof("price:%s", price)
if price != "" && validator.IsNumberStr(price) {
break
}
}
return nil
}))
tasks = append(tasks, chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery))
err := chromedp.Run(ctx, tasks)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
var messages []string
document.Find("div.quote-text-border,span.quote-price").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Info(text)
messages = append(messages, text)
})
return &messages
}
func SearchStockInfo(stock, msgType string) *[]string {
// 创建一个 chromedp 上下文
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
defer cancel()
var htmlContent string
url := fmt.Sprintf("https://www.cls.cn/searchPage?keyword=%s&type=%s", stock, msgType)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
//chromedp.Sleep(3*time.Second),
chromedp.WaitVisible("div.search-content,a.search-content", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
var messages []string
document.Find("div.search-telegraph-list,a.search-content").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
if strings.Contains(text, stock) {
messages = append(messages, text)
logger.SugaredLogger.Infof("搜索到消息: %s", text)
}
})
return &messages
}

View File

@@ -2,11 +2,17 @@ package data
import (
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"io/ioutil"
"strings"
"testing"
"time"
)
// @Author spark
@@ -14,9 +20,63 @@ import (
// @Desc
//-----------------------------------------------------------------------------------
func TestGetTelegraph(t *testing.T) {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return
}
logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return
}
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
text := selection.Text()
logger.SugaredLogger.Info(text)
})
}
func TestGetTelegraphSearch(t *testing.T) {
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
messages := SearchStockInfo("闻泰科技", "depth")
for _, message := range *messages {
logger.SugaredLogger.Info(message)
}
//https://www.cls.cn/stock?code=sh600745
}
func TestSearchStockPriceInfo(t *testing.T) {
SearchStockPriceInfo("sh600745")
}
func TestParseFullSingleStockData(t *testing.T) {
resp, err := resty.New().R().
SetHeader("Host", "hq.sinajs.cn").
SetHeader("Referer", "https://finance.sina.com.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600859,sh600745"))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
data := GB18030ToUTF8(resp.Body())
strs := strutil.SplitEx(data, "\n", true)
for _, str := range strs {
logger.SugaredLogger.Info(str)
}
}
func TestNewStockDataApi(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
t.Log(stockDataApi.GetStockCodeRealTimeData("sh600859"))
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745")
for _, data := range *datas {
t.Log(data)
}
}
func TestGetStockBaseInfo(t *testing.T) {
@@ -74,7 +134,7 @@ func TestReadFile(t *testing.T) {
func TestFollowedList(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
t.Log(stockDataApi.GetFollowList())
stockDataApi.GetFollowList()
}

BIN
build/screenshot/img_10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
build/screenshot/img_11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
build/screenshot/img_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
build/screenshot/img_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
build/screenshot/img_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
build/screenshot/img_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
build/screenshot/img_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
build/screenshot/img_9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -5,19 +5,19 @@
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "{{.Name}}"
!define INFO_PROJECTNAME "go-stock"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
!define INFO_COMPANYNAME "sparkmemory"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
!define INFO_PRODUCTNAME "go-stock"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
!define INFO_COPYRIGHT "Copyright#sparkmemory@163.com"
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
@@ -203,20 +203,12 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!macro wails.associateFiles
; Create file associations
{{range .Info.FileAssociations}}
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
File "..\{{.IconName}}.ico"
{{end}}
!macroend
!macro wails.unassociateFiles
; Delete app associations
{{range .Info.FileAssociations}}
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
Delete "$INSTDIR\{{.IconName}}.ico"
{{end}}
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
@@ -235,15 +227,10 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!macro wails.associateCustomProtocols
; Create custom protocols associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
{{end}}
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
{{end}}
!macroend

View File

@@ -9,11 +9,13 @@
"version": "1.0.0",
"dependencies": {
"@vicons/ionicons5": "^0.13.0",
"vue": "^3.2.25"
"md-editor-v3": "^5.2.1",
"vue": "^3.2.25",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"naive-ui": "^2.40.3",
"naive-ui": "^2.41.0",
"vfonts": "^0.0.3",
"vite": "5.4.6"
}
@@ -60,6 +62,360 @@
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.4",
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz",
"integrity": "sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.0",
"resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.8.0.tgz",
"integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-angular": {
"version": "0.1.3",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-angular/-/lang-angular-0.1.3.tgz",
"integrity": "sha512-xgeWGJQQl1LyStvndWtruUvb4SnBZDAu/gvFH/ZU+c0W25tQR8e5hq7WTwiIY2dNxnf+49mRiGI/9yxIwB6f5w==",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/language": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.3"
}
},
"node_modules/@codemirror/lang-cpp": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz",
"integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/cpp": "^1.0.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-go": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
"integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/go": "^1.0.0"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.9",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.0"
}
},
"node_modules/@codemirror/lang-java": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-java/-/lang-java-6.0.1.tgz",
"integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/java": "^1.0.0"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
"integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/javascript": "^1.0.0"
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-less": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-less/-/lang-less-6.0.2.tgz",
"integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==",
"dependencies": {
"@codemirror/lang-css": "^6.2.0",
"@codemirror/language": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-liquid": {
"version": "6.2.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-liquid/-/lang-liquid-6.2.2.tgz",
"integrity": "sha512-7Dm841fk37+JQW6j2rI1/uGkJyESrjzyhiIkaLjbbR0U6aFFQvMrJn35WxQreRMADMhzkyVkZM4467OR7GR8nQ==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.1"
}
},
"node_modules/@codemirror/lang-markdown": {
"version": "6.3.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-markdown/-/lang-markdown-6.3.2.tgz",
"integrity": "sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/lang-php": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-php/-/lang-php-6.0.1.tgz",
"integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/php": "^1.0.0"
}
},
"node_modules/@codemirror/lang-python": {
"version": "6.1.6",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-python/-/lang-python-6.1.6.tgz",
"integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==",
"dependencies": {
"@codemirror/autocomplete": "^6.3.2",
"@codemirror/language": "^6.8.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/python": "^1.1.4"
}
},
"node_modules/@codemirror/lang-rust": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz",
"integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/rust": "^1.0.0"
}
},
"node_modules/@codemirror/lang-sass": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz",
"integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==",
"dependencies": {
"@codemirror/lang-css": "^6.2.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/sass": "^1.0.0"
}
},
"node_modules/@codemirror/lang-sql": {
"version": "6.8.0",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz",
"integrity": "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-vue": {
"version": "0.1.3",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz",
"integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/language": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.1"
}
},
"node_modules/@codemirror/lang-wast": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz",
"integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-xml": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/xml": "^1.0.0"
}
},
"node_modules/@codemirror/lang-yaml": {
"version": "6.1.2",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.0.0",
"@lezer/yaml": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.10.8",
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.10.8.tgz",
"integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/language-data": {
"version": "6.5.1",
"resolved": "https://registry.npmmirror.com/@codemirror/language-data/-/language-data-6.5.1.tgz",
"integrity": "sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==",
"dependencies": {
"@codemirror/lang-angular": "^0.1.0",
"@codemirror/lang-cpp": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-go": "^6.0.0",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-java": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/lang-json": "^6.0.0",
"@codemirror/lang-less": "^6.0.0",
"@codemirror/lang-liquid": "^6.0.0",
"@codemirror/lang-markdown": "^6.0.0",
"@codemirror/lang-php": "^6.0.0",
"@codemirror/lang-python": "^6.0.0",
"@codemirror/lang-rust": "^6.0.0",
"@codemirror/lang-sass": "^6.0.0",
"@codemirror/lang-sql": "^6.0.0",
"@codemirror/lang-vue": "^0.1.1",
"@codemirror/lang-wast": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/lang-yaml": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.4.0"
}
},
"node_modules/@codemirror/legacy-modes": {
"version": "6.4.2",
"resolved": "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.4.2.tgz",
"integrity": "sha512-HsvWu08gOIIk303eZQCal4H4t65O/qp1V4ul4zVa3MHK5FJ0gz3qz3O55FIkm+aQUcshUOjBx38t2hPiJwW5/g==",
"dependencies": {
"@codemirror/language": "^6.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.4",
"resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.4.tgz",
"integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.8",
"resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.8.tgz",
"integrity": "sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.1",
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.1.tgz",
"integrity": "sha512-3rA9lcwciEB47ZevqvD8qgbzhM9qMb8vCcQCNmDfVRPQG4JT9mSb0Jg8H7YjKGGQcFnLN323fj9jdnG59Kx6bg==",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.36.2",
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.36.2.tgz",
"integrity": "sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==",
"dependencies": {
"@codemirror/state": "^6.5.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@css-render/plugin-bem": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz",
@@ -463,6 +819,171 @@
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"dev": true
},
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
},
"node_modules/@lezer/cpp": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@lezer/cpp/-/cpp-1.1.2.tgz",
"integrity": "sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/css": {
"version": "1.1.9",
"resolved": "https://registry.npmmirror.com/@lezer/css/-/css-1.1.9.tgz",
"integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/go": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/@lezer/go/-/go-1.0.0.tgz",
"integrity": "sha512-co9JfT3QqX1YkrMmourYw2Z8meGC50Ko4d54QEcQbEYpvdUvN4yb0NBZdn/9ertgvjsySxHsKzH3lbm3vqJ4Jw==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.10",
"resolved": "https://registry.npmmirror.com/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/java": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/@lezer/java/-/java-1.1.3.tgz",
"integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
"version": "1.4.21",
"resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.4.21.tgz",
"integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/json": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.4.0.tgz",
"integrity": "sha512-mk4MYeq6ZQdxgsgRAe0G7kqPRV6Desajfa14TcHoGGXIqqj1/2ARN31VFpmrXDgvXiGBWpA7RXtv0he+UdTkGw==",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@lezer/php": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@lezer/php/-/php-1.0.2.tgz",
"integrity": "sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.1.0"
}
},
"node_modules/@lezer/python": {
"version": "1.1.15",
"resolved": "https://registry.npmmirror.com/@lezer/python/-/python-1.1.15.tgz",
"integrity": "sha512-aVQ43m2zk4FZYedCqL0KHPEUsqZOrmAvRhkhHlVPnDD1HODDyyQv5BRIuod4DadkgBEZd53vQOtXTonNbEgjrQ==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/rust": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@lezer/rust/-/rust-1.0.2.tgz",
"integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/sass": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/@lezer/sass/-/sass-1.0.7.tgz",
"integrity": "sha512-8HLlOkuX/SMHOggI2DAsXUw38TuURe+3eQ5hiuk9QmYOUyC55B1dYEIMkav5A4IELVaW4e1T4P9WRiI5ka4mdw==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/xml": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/@lezer/xml/-/xml-1.0.6.tgz",
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/yaml": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.3.tgz",
"integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.4.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.28.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
@@ -722,6 +1243,11 @@
"integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
"dev": true
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
},
"node_modules/@types/lodash": {
"version": "4.17.13",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.13.tgz",
@@ -737,6 +1263,25 @@
"@types/lodash": "*"
}
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
},
"node_modules/@vavt/util": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/@vavt/util/-/util-2.1.0.tgz",
"integrity": "sha512-YIfAvArSFVXmWvoF+DEGD0FhkhVNcCtVWWkfYtj76eSrwHh/wuEEFhiEubg1XLNM3tChO8FH8xJCT/hnizjgFQ=="
},
"node_modules/@vicons/ionicons5": {
"version": "0.13.0",
"resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz",
@@ -801,6 +1346,11 @@
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
},
"node_modules/@vue/reactivity": {
"version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz",
@@ -846,12 +1396,49 @@
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"dev": true
},
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"node_modules/css-render": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz",
@@ -868,6 +1455,11 @@
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
"dev": true
},
"node_modules/cssfilter": {
"version": "0.0.10",
"resolved": "https://registry.npmmirror.com/cssfilter/-/cssfilter-0.0.10.tgz",
"integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
@@ -975,6 +1567,14 @@
"node": ">=12.0.0"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
@@ -987,6 +1587,22 @@
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"dev": true
},
"node_modules/lru-cache": {
"version": "11.0.2",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.0.2.tgz",
"integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/lucide-vue-next": {
"version": "0.453.0",
"resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.453.0.tgz",
"integrity": "sha512-5zmv83vxAs9SVoe22veDBi8Dw0Fh2F+oTngWgKnKOkrZVbZjceXLQ3tescV2boB0zlaf9R2Sd9RuUP2766xvsQ==",
"peerDependencies": {
"vue": ">=3.0.1"
}
},
"node_modules/magic-string": {
"version": "0.30.14",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.14.tgz",
@@ -995,10 +1611,81 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it-image-figures": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/markdown-it-image-figures/-/markdown-it-image-figures-2.1.1.tgz",
"integrity": "sha512-mwXSQ2nPeVUzCMIE3HlLvjRioopiqyJLNph0pyx38yf9mpqFDhNGnMpAXF9/A2Xv0oiF2cVyg9xwfF0HNAz05g==",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"markdown-it": "*"
}
},
"node_modules/markdown-it-sub": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/markdown-it-sub/-/markdown-it-sub-2.0.0.tgz",
"integrity": "sha512-iCBKgwCkfQBRg2vApy9vx1C1Tu6D8XYo8NvevI3OlwzBRmiMtsJ2sXupBgEA7PPxiDwNni3qIUkhZ6j5wofDUA=="
},
"node_modules/markdown-it-sup": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/markdown-it-sup/-/markdown-it-sup-2.0.0.tgz",
"integrity": "sha512-5VgmdKlkBd8sgXuoDoxMpiU+BiEt3I49GItBzzw7Mxq9CxvnhE/k09HFli09zgfFDRixDQDfDxi0mgBCXtaTvA=="
},
"node_modules/md-editor-v3": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/md-editor-v3/-/md-editor-v3-5.2.1.tgz",
"integrity": "sha512-FPbVEO/9xovXM5OULfBPdM22LrKevEHnkjmYF0pqi34UF/Wt3d5LW/UX2DehwCTmYua6N7385K93un58K69xPA==",
"dependencies": {
"@codemirror/lang-markdown": "^6.3.0",
"@codemirror/language-data": "^6.5.1",
"@types/markdown-it": "^14.0.1",
"@vavt/util": "^2.1.0",
"codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3",
"lru-cache": "^11.0.1",
"lucide-vue-next": "^0.453.0",
"markdown-it": "^14.0.0",
"markdown-it-image-figures": "^2.1.1",
"markdown-it-sub": "^2.0.0",
"markdown-it-sup": "^2.0.0",
"medium-zoom": "^1.1.0",
"xss": "^1.0.15"
},
"peerDependencies": {
"vue": "^3.5.3"
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
},
"node_modules/medium-zoom": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/medium-zoom/-/medium-zoom-1.1.0.tgz",
"integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ=="
},
"node_modules/naive-ui": {
"version": "2.40.3",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.40.3.tgz",
"integrity": "sha512-TpgYfOg0SNlG4HHhTdFnFcPc1trZiX3r10Pn6biyEgRoi6ZC5qbsY8xgKsqQuG4nWj2PHLT8pPVEkt2pKOlxag==",
"version": "2.41.0",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.41.0.tgz",
"integrity": "sha512-KnmLg+xPLwXV8QVR7ZZ69eCjvel7R5vru8+eFe4VoAJHEgqAJgVph6Zno9K2IVQRpSF3GBGea3tjavslOR4FAA==",
"dev": true,
"dependencies": {
"@css-render/plugin-bem": "^0.15.14",
@@ -1074,6 +1761,14 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"engines": {
"node": ">=6"
}
},
"node_modules/rollup": {
"version": "4.28.1",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.28.1.tgz",
@@ -1126,12 +1821,27 @@
"node": ">=0.10.0"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
},
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"node_modules/treemate": {
"version": "0.3.11",
"resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz",
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
"dev": true
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
},
"node_modules/vdirs": {
"version": "0.1.8",
"resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz",
@@ -1241,6 +1951,20 @@
}
}
},
"node_modules/vue-router": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz",
"integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vueuc": {
"version": "0.4.64",
"resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.64.tgz",
@@ -1258,6 +1982,26 @@
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
},
"node_modules/xss": {
"version": "1.0.15",
"resolved": "https://registry.npmmirror.com/xss/-/xss-1.0.15.tgz",
"integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==",
"dependencies": {
"commander": "^2.20.3",
"cssfilter": "0.0.10"
},
"bin": {
"xss": "bin/xss"
},
"engines": {
"node": ">= 0.10.0"
}
}
}
}

View File

@@ -10,14 +10,16 @@
},
"dependencies": {
"@vicons/ionicons5": "^0.13.0",
"vue": "^3.2.25"
"md-editor-v3": "^5.2.1",
"vue": "^3.2.25",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"naive-ui": "^2.40.3",
"naive-ui": "^2.41.0",
"vfonts": "^0.0.3",
"vite": "5.4.6"
},
"keywords": [],
"keywords": ["AI赋能股票分析","go-stock"],
"author": "spark"
}

View File

@@ -1 +1 @@
92d1e03303e475c85c8fdd77128fc32c
39a415166f03acc0270e24443a9e2445

View File

@@ -1,34 +1,195 @@
<script setup>
import stockInfo from './components/stock.vue'
import {ref} from "vue";
import { darkTheme } from 'naive-ui'
import {
EventsOn,
Quit,
WindowFullscreen, WindowGetPosition,
WindowHide,
WindowIsFullscreen, WindowSetPosition,
WindowUnfullscreen
} from '../wailsjs/runtime'
import {h, ref} from "vue";
import { RouterLink } from 'vue-router'
import {darkTheme, NIcon, NText,} from 'naive-ui'
import {
SettingsOutline,
ReorderTwoOutline,
ExpandOutline,
RefreshOutline, PowerOutline, BarChartOutline, MoveOutline, WalletOutline, StarOutline,
} from '@vicons/ionicons5'
const content = ref('数据来源于网络,仅供参考\n投资有风险,入市需谨慎')
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
const isFullscreen = ref(false)
const activeKey = ref('stock')
const containerRef= ref({})
const realtimeProfit= ref(0)
const telegraph= ref([])
const menuOptions = ref([
{
label: () =>
h(
RouterLink,
{
to: {
name: 'stock',
params: {
id: 'zh-CN'
},
}
},
{ default: () => '我的自选',}
),
key: 'stock',
icon: renderIcon(StarOutline),
children:[
{
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
key: 'realtimeProfit',
show: realtimeProfit.value,
icon: renderIcon(WalletOutline),
},
]
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'settings',
params: {
id: 'zh-CN'
}
}
},
{ default: () => '设置' }
),
key: 'settings',
icon: renderIcon(SettingsOutline),
},
{
label: ()=> h("a", {
href: '#',
onClick: toggleFullscreen,
title: '全屏 Ctrl+F 退出全屏 Esc',
}, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
key: 'full',
icon: renderIcon(ExpandOutline),
},
{
label: ()=> h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H',
}, { default: () => '隐藏到托盘区' }),
key: 'hide',
icon: renderIcon(ReorderTwoOutline),
},
{
label: ()=> h("a", {
href: 'javascript:void(0)',
style: 'cursor: move;',
onClick: toggleStartMoveWindow,
}, { default: () => '移动' }),
key: 'move',
icon: renderIcon(MoveOutline),
},
{
label: ()=> h("a", {
href: '#',
onClick: Quit,
}, { default: () => '退出程序' }),
key: 'exit',
icon: renderIcon(PowerOutline),
},
])
function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(icon) })
}
function toggleFullscreen(e) {
//console.log(e)
if (isFullscreen.value) {
WindowUnfullscreen()
//e.target.innerHTML = '全屏'
} else {
WindowFullscreen()
// e.target.innerHTML = '取消全屏'
}
isFullscreen.value=!isFullscreen.value
}
const drag = ref(false)
const lastPos= ref({x:0,y:0})
function toggleStartMoveWindow(e) {
drag.value=!drag.value
lastPos.value={x:e.clientX,y:e.clientY}
}
function dragstart(e) {
if (drag.value) {
let x=e.clientX-lastPos.value.x
let y=e.clientY-lastPos.value.y
WindowGetPosition().then((pos) => {
WindowSetPosition(pos.x+x,pos.y+y)
})
}
}
window.addEventListener('mousemove', dragstart)
EventsOn("realtime_profit",(data)=>{
realtimeProfit.value=data
})
EventsOn("telegraph",(data)=>{
telegraph.value=data
})
</script>
<template>
<n-config-provider :theme="darkTheme">
<n-config-provider :theme="darkTheme" ref="containerRef">
<n-message-provider >
<n-notification-provider>
<n-modal-provider>
<n-watermark
:content="content"
cross
selectable
:font-size="12"
:line-height="12"
:font-size="16"
:line-height="16"
:width="500"
:height="400"
:width="200"
:x-offset="50"
:y-offset="50"
:y-offset="150"
:rotate="-15"
style="height: 100%"
>
<n-flex justify="center">
<n-message-provider >
<n-modal-provider>
<stockInfo/>
</n-modal-provider>
</n-message-provider>
<n-grid x-gap="12" :cols="1">
<n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
<n-marquee :speed="120" >
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
{{item}}
</n-tag>
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
</n-marquee>
</n-gi>
<n-gi style="padding-bottom: 70px;padding-top: 5px">
<RouterView />
</n-gi>
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%">
<n-card size="small">
<n-menu style="font-size: 18px;"
v-model:value="activeKey"
mode="horizontal"
:options="menuOptions"
responsive
/>
</n-card>
</n-gi>
</n-grid>
</n-flex>
</n-watermark>
</n-modal-provider>
</n-notification-provider>
</n-message-provider>
</n-config-provider>
</template>
<style>

View File

@@ -0,0 +1,194 @@
<script setup>
import {onMounted, ref, watch} from "vue";
import {GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
import {useMessage} from "naive-ui";
import {data} from "../../wailsjs/go/models";
const message = useMessage()
const formRef = ref(null)
const formValue = ref({
ID:1,
tushareToken:'',
dingPush:{
enable:false,
dingRobot: ''
},
localPush:{
enable:true,
},
updateBasicInfoOnStart:false,
refreshInterval:1,
openAI:{
enable:false,
baseUrl: 'https://api.deepseek.com',
apiKey: '',
model: 'deepseek-chat',
temperature: 0.1,
maxTokens: 1024,
prompt:"",
},
})
onMounted(()=>{
GetConfig().then(res=>{
formValue.value.ID = res.ID
formValue.value.tushareToken = res.tushareToken
formValue.value.dingPush = {
enable:res.dingPushEnable,
dingRobot:res.dingRobot
}
formValue.value.localPush = {
enable:res.localPushEnable,
}
formValue.value.updateBasicInfoOnStart = res.updateBasicInfoOnStart
formValue.value.refreshInterval = res.refreshInterval
formValue.value.openAI = {
enable:res.openAiEnable,
baseUrl: res.openAiBaseUrl,
apiKey:res.openAiApiKey,
model:res.openAiModelName,
temperature:res.openAiTemperature,
maxTokens:res.openAiMaxTokens,
prompt:res.prompt,
}
console.log(res)
})
//message.info("加载完成")
})
function saveConfig(){
let config= new data.Settings({
ID:formValue.value.ID,
dingPushEnable:formValue.value.dingPush.enable,
dingRobot:formValue.value.dingPush.dingRobot,
localPushEnable:formValue.value.localPush.enable,
updateBasicInfoOnStart:formValue.value.updateBasicInfoOnStart,
refreshInterval:formValue.value.refreshInterval,
openAiEnable:formValue.value.openAI.enable,
openAiBaseUrl:formValue.value.openAI.baseUrl,
openAiApiKey:formValue.value.openAI.apiKey,
openAiModelName:formValue.value.openAI.model,
openAiMaxTokens:formValue.value.openAI.maxTokens,
openAiTemperature:formValue.value.openAI.temperature,
tushareToken:formValue.value.tushareToken,
prompt:formValue.value.openAI.prompt
})
//console.log("Settings",config)
UpdateConfig(config).then(res=>{
message.success(res)
})
}
function getHeight() {
return document.documentElement.clientHeight
}
function sendTestNotice(){
let markdown="### go-stock test\n"+new Date()
let msg='{' +
' "msgtype": "markdown",' +
' "markdown": {' +
' "title":"go-stock'+new Date()+'",' +
' "text": "'+markdown+'"' +
' },' +
' "at": {' +
' "isAtAll": true' +
' }' +
' }'
SendDingDingMessageByType(msg, "test-"+new Date().getTime(),1).then(res=>{
message.info(res)
})
}
</script>
<template>
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px">
<n-form ref="formRef" :model="formValue" :label-placement="'left'" :label-align="'left'" style="width: 100%;height: 100%">
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24">
<n-text type="default" style="font-size: 25px;font-weight: bold">基础设置</n-text>
</n-gi>
<n-form-item-gi :span="10" label="Tushare api token" path="tushareToken" >
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
</n-form-item-gi>
<n-form-item-gi :span="6" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
</n-form-item-gi>
<n-form-item-gi :span="6" label="数据刷新间隔(重启生效)" path="refreshInterval" >
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
<template #suffix>
</template>
</n-input-number>
</n-form-item-gi>
</n-grid>
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24">
<n-text type="default" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi>
<n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" >
<n-switch v-model:value="formValue.dingPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="6" label="是否启用本地推送:" path="localPush.enable" >
<n-switch v-model:value="formValue.localPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
</n-form-item-gi>
</n-grid>
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
<n-gi :span="24">
<n-text type="default" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
</n-gi>
<n-form-item-gi :span="6" label="是否启用AI诊股" path="openAI.enable" >
<n-switch v-model:value="formValue.openAI.enable" />
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="openAI apiKey" path="openAI.apiKey">
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="AI模型" path="openAI.model">
<n-input type="text" placeholder="AI模型" v-model:value="formValue.openAI.model" clearable />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="temperature" path="openAI.temperature" >
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="maxTokens" path="openAI.maxTokens">
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="自定义Prompt" path="openAI.prompt">
<n-input v-model:value="formValue.openAI.prompt"
type="textarea"
:show-count="true"
placeholder="请输入prompt"
:autosize="{
minRows: 5,
maxRows: 8
}"
/>
</n-form-item-gi>
</n-grid>
<n-gi :span="24">
<div style="display: flex; justify-content: center">
<n-button type="primary" @click="saveConfig">
保存
</n-button>
</div>
</n-gi>
</n-form>
</n-flex>
</template>
<style scoped>
</style>

View File

@@ -1,20 +1,25 @@
<script setup>
import {onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
import {computed, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
import {
Greet,
Follow,
UnFollow,
Follow, GetConfig,
GetFollowList,
GetStockList,
SetCostPriceAndVolume,
SendDingDingMessage, SetAlarmChangePercent
Greet, NewChat, NewChatStream,
SendDingDingMessage, SendDingDingMessageByType,
SetAlarmChangePercent,
SetCostPriceAndVolume, SetStockSort,
UnFollow
} from '../../wailsjs/go/main/App'
import {NButton, NFlex, NForm, NFormItem, NInputNumber, NText, useMessage, useModal} from 'naive-ui'
import { WindowFullscreen,WindowUnfullscreen,EventsOn } from '../../wailsjs/runtime'
import {Add, StarOutline} from '@vicons/ionicons5'
import {NButton, NFlex, NForm, NFormItem, NInputNumber, NText, useMessage, useModal,useNotification} from 'naive-ui'
import {EventsOn, WindowFullscreen, WindowReload, WindowUnfullscreen} from '../../wailsjs/runtime'
import {Add, Search,StarOutline} from '@vicons/ionicons5'
import { MdPreview } from 'md-editor-v3';
// preview.css相比style.css少了编辑器那部分样式
import 'md-editor-v3/lib/preview.css';
const mdPreviewRef = ref(null)
const message = useMessage()
const modal = useModal()
const notify = useNotification()
const stocks=ref([])
const results=ref({})
@@ -25,6 +30,7 @@ const options=ref([])
const modalShow = ref(false)
const modalShow2 = ref(false)
const modalShow3 = ref(false)
const modalShow4 = ref(false)
const addBTN = ref(true)
const formModel = ref({
name: "",
@@ -32,6 +38,8 @@ const formModel = ref({
costPrice: 0.000,
volume: 0,
alarm: 0,
alarmPrice:0,
sort:999,
})
const data = reactive({
@@ -41,8 +49,20 @@ const data = reactive({
kURL:"",
resultText: "Please enter your name below 👇",
fullscreen: false,
airesult: "",
openAiEnable: false,
loading: true,
})
const sortedResults = computed(() => {
//console.log("computed",sortedResults.value)
const sortedKeys =Object.keys(results.value).sort();
const sortedObject = {};
sortedKeys.forEach(key => {
sortedObject[key] = results.value[key];
});
return sortedObject
});
onBeforeMount(()=>{
GetStockList("").then(result => {
@@ -62,25 +82,29 @@ onBeforeMount(()=>{
}
}
monitor()
message.destroyAll
message.destroyAll()
})
GetConfig().then(result => {
if (result.openAiEnable) {
data.openAiEnable = true
}
})
})
onMounted(() => {
message.loading("Loading...")
console.log(`the component is now mounted.`)
// console.log(`the component is now mounted.`)
ticker.value=setInterval(() => {
if(isTradingTime()){
monitor()
//monitor()
data.fenshiURL='http://image.sinajs.cn/newchart/min/n/'+data.code+'.gif'+"?t="+Date.now()
}
}, 3500)
})
onBeforeUnmount(() => {
console.log(`the component is now unmounted.`)
// console.log(`the component is now unmounted.`)
clearInterval(ticker.value)
})
@@ -92,21 +116,40 @@ EventsOn("showSearch",(data)=>{
addBTN.value = data === 1;
})
EventsOn("stock_price",(data)=>{
//console.log("stock_price",data['股票代码'])
updateData(data)
})
EventsOn("refreshFollowList",(data)=>{
message.loading("refresh...")
GetFollowList().then(result => {
followList.value = result
for (const followedStock of result) {
if (!stocks.value.includes(followedStock.StockCode)) {
stocks.value.push(followedStock.StockCode)
}
}
monitor()
message.destroyAll
})
WindowReload()
// message.loading("refresh...")
// GetFollowList().then(result => {
// followList.value = result
// for (const followedStock of result) {
// if (!stocks.value.includes(followedStock.StockCode)) {
// stocks.value.push(followedStock.StockCode)
// }
// }
// monitor()
// message.destroyAll
// })
})
EventsOn("newChatStream",async (msg) => {
//console.log("newChatStream:->",data.airesult)
data.loading = false
if (msg === "DONE") {
message.info("AI分析完成")
message.destroyAll()
} else {
data.airesult = data.airesult + msg
}
})
//判断是否是A股交易时间
function isTradingTime() {
const now = new Date();
@@ -141,19 +184,24 @@ function AddStock(){
function removeMonitor(code,name) {
console.log("removeMonitor",name,code)
function removeMonitor(code,name,key) {
console.log("removeMonitor",name,code,key)
stocks.value.splice(stocks.value.indexOf(code),1)
delete results.value[name]
console.log("removeMonitor-key",key)
console.log("removeMonitor-v",results.value[key])
delete results.value[key]
console.log("removeMonitor-v",results.value[key])
UnFollow(code).then(result => {
message.success(result)
})
}
function getStockList(value){
console.log("getStockList",value)
// console.log("getStockList",value)
let result;
result=stockList.value.filter(item => item.name.includes(data.name)||item.ts_code.includes(data.name))
result=stockList.value.filter(item => item.name.includes(value)||item.ts_code.includes(value))
options.value=result.map(item => {
return {
label: item.name+" - "+item.ts_code,
@@ -165,54 +213,66 @@ function getStockList(value){
}
}
async function updateData(result) {
if(result["当前价格"]<=0){
result["当前价格"]=result["卖一报价"]
}
if (result.changePercent>0) {
result.type="error"
result.color="#E88080"
}else if (result.changePercent<0) {
result.type="success"
result.color="#63E2B7"
}else {
result.type="default"
result.color="#FFFFFF"
}
if(result.profitAmount>0){
result.profitType="error"
}else if(result.profitAmount<0){
result.profitType="success"
}
if(result["当前价格"]){
if(result.alarmChangePercent>0&&Math.abs(result.changePercent)>=result.alarmChangePercent){
SendMessage(result,1)
}
if(result.alarmPrice>0&&result["当前价格"]>=result.alarmPrice){
SendMessage(result,2)
}
if(result.costPrice>0&&result["当前价格"]>=result.costPrice){
SendMessage(result,3)
}
}
result.key=GetSortKey(result.sort,result["股票代码"])
results.value[GetSortKey(result.sort,result["股票代码"])]=result
}
async function monitor() {
for (let code of stocks.value) {
// console.log(code)
Greet(code).then(result => {
if(result["当前价格"]<=0){
result["当前价格"]=result["卖一报价"]
}
let s=(result["当前价格"]-result["昨日收盘价"])*100/result["昨日收盘价"]
let roundedNum = s.toFixed(2); // 将数字转换为保留两位小数的字符串形式
result.s=roundedNum+"%"
result.highRate=((result["今日最高价"]-result["今日开盘价"])*100/result["今日开盘价"]).toFixed(2)+"%"
result.lowRate=((result["今日最低价"]-result["今日开盘价"])*100/result["今日开盘价"]).toFixed(2)+"%"
if (roundedNum>0) {
result.type="error"
result.color="#E88080"
}else if (roundedNum<0) {
result.type="success"
result.color="#63E2B7"
}else {
result.type="default"
result.color="#FFFFFF"
}
let res= followList.value.filter(item => item.StockCode===code)
if (res.length>0) {
result.Sort=res[0].Sort
result.costPrice=res[0].CostPrice
result.volume=res[0].Volume
result.profit=((result["当前价格"]-result.costPrice)*100/result.costPrice).toFixed(3)
result.profitAmountToday=(result.volume*(result["当前价格"]-result["昨日收盘价"])).toFixed(2)
result.profitAmount=(result.volume*(result["当前价格"]-result.costPrice)).toFixed(2)
if(result.profitAmount>0){
result.profitType="error"
}else if(result.profitAmount<0){
result.profitType="success"
}
if(res[0].AlarmChangePercent>0&&Math.abs(roundedNum)>res[0].AlarmChangePercent){
SendMessage(result)
}
}
results.value[result["股票名称"]]=result
updateData(result)
})
}
}
//数字长度不够前面补0
function padZero(num, length) {
return (Array(length).join('0') + num).slice(-length);
}
function GetSortKey(sort,code){
return padZero(sort,6)+"_"+code
}
function onSelect(item) {
console.log("onSelect",item)
//console.log("onSelect",item)
if(item.indexOf("-")>0){
item=item.split("-")[1].toLowerCase()
@@ -225,17 +285,24 @@ function onSelect(item) {
function search(code,name){
setTimeout(() => {
window.open("https://xueqiu.com/S/"+code)
//window.open("https://xueqiu.com/S/"+code)
//window.open("https://www.cls.cn/stock?code="+code)
//window.open("https://quote.eastmoney.com/"+code+".html")
//window.open("https://finance.sina.com.cn/realstock/company/"+code+"/nc.shtml")
window.open("https://www.iwencai.com/unifiedwap/result?w="+code)
//window.open("https://www.iwencai.com/chat/?question="+code)
}, 500)
}
function setStock(code,name){
let res=followList.value.filter(item => item.StockCode===code)
console.log("res:",res)
//console.log("res:",res)
formModel.value.name=name
formModel.value.code=code
formModel.value.volume=res[0].Volume
formModel.value.costPrice=res[0].CostPrice
formModel.value.alarm=res[0].AlarmChangePercent
formModel.value.alarmPrice=res[0].AlarmPrice
formModel.value.sort=res[0].Sort
modalShow.value=true
}
@@ -253,10 +320,16 @@ function showK(code,name){
}
function updateCostPriceAndVolumeNew(code,price,volume,alarm){
console.log(code,price,volume)
if(alarm){
SetAlarmChangePercent(alarm,code).then(result => {
function updateCostPriceAndVolumeNew(code,price,volume,alarm,formModel){
if(formModel.sort){
SetStockSort(formModel.sort,code).then(result => {
//message.success(result)
})
}
if(alarm||formModel.alarmPrice){
SetAlarmChangePercent(alarm,formModel.alarmPrice,code).then(result => {
//message.success(result)
})
}
@@ -271,7 +344,7 @@ function updateCostPriceAndVolumeNew(code,price,volume,alarm){
}
}
monitor()
message.destroyAll
message.destroyAll()
})
})
}
@@ -285,50 +358,95 @@ function fullscreen(){
data.fullscreen=!data.fullscreen
}
function SendMessage(result){
//type 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
function SendMessage(result,type){
let typeName=getTypeName(type)
let img='http://image.sinajs.cn/newchart/min/n/'+result["股票代码"]+'.gif'+"?t="+Date.now()
let markdown="### go-stock市场行情\n\n"+
let markdown="### go-stock ["+typeName+"]\n\n"+
"### "+result["股票名称"]+"("+result["股票代码"]+")\n" +
"- 当前价格: "+result["当前价格"]+" "+result.s+"\n" +
"- 当前价格: "+result["当前价格"]+" "+result.changePercent+"%\n" +
"- 最高价: "+result["今日最高价"]+" "+result.highRate+"\n" +
"- 最低价: "+result["今日最低价"]+" "+result.lowRate+"\n" +
"- 昨收价: "+result["昨日收盘价"]+"\n" +
"- 今开价: "+result["今日开盘价"]+"\n" +
"- 成本价: "+result.costPrice+" "+result.profit+"% "+result.profitAmount+" ¥\n" +
"- 成本数量: "+result.volume+"股\n" +
"- 成本数量: "+result.costVolume+"股\n" +
"- 日期: "+result["日期"]+" "+result["时间"]+"\n\n"+
"![image]("+img+")\n"
let title=result["股票名称"]+"("+result["股票代码"]+") "+result["当前价格"]+" "+result.changePercent
let msg='{' +
' "msgtype": "markdown",' +
' "markdown": {' +
' "title":"'+result["股票名称"]+"("+result["股票代码"]+") "+result["当前价格"]+" "+result.s+'",' +
' "title":"['+typeName+"]"+title+'",' +
' "text": "'+markdown+'"' +
' },' +
' "at": {' +
' "isAtAll": true' +
' }' +
' }'
SendDingDingMessage(msg,result["股票代码"])
// SendDingDingMessage(msg,result["股票代码"])
SendDingDingMessageByType(msg,result["股票代码"],type)
}
function aiCheckStock(stock,stockCode){
data.airesult=""
data.name=stock
data.code=stockCode
data.loading=true
modalShow4.value=true
message.loading("ai检测中...",{
duration: 0,
})
NewChatStream(stock,stockCode)
}
function getTypeName(type){
switch (type)
{
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return ""
}
}
//获取高度
function getHeight() {
return document.documentElement.clientHeight
}
</script>
<template>
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
<n-gi v-for="result in results" >
<n-card :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :closable="true" @close="removeMonitor(result['股票代码'],result['股票名称'])">
<n-gi v-for="result in sortedResults" >
<n-card :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
<n-grid :cols="1" :y-gap="6">
<n-gi>
<n-text :type="result.type" >{{result["当前价格"]}}</n-text><n-text style="padding-left: 10px;" :type="result.type">{{ result.s}}</n-text>&nbsp;
<n-text size="small" v-if="result.profitAmountToday>0" :type="result.type">{{result.profitAmountToday}}</n-text>
<n-text :type="result.type" >
<n-number-animation :duration="1000" :precision="2" :from="result['上次当前价格']" :to="Number(result['当前价格'])" />
</n-text>
<n-text style="padding-left: 10px;" :type="result.type">
<n-number-animation :duration="1000" :precision="3" :from="0" :to="result.changePercent" />%
</n-text>&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-text :type="'info'">{{"最高 "+result["今日最高价"]+" "+result.highRate }}%</n-text>
</n-gi>
<n-gi>
<n-text :type="'info'">{{"最低 "+result["今日最低价"]+" "+result.lowRate }}</n-text>
<n-text :type="'info'">{{"最低 "+result["今日最低价"]+" "+result.lowRate }}%</n-text>
</n-gi>
<n-gi>
<n-text :type="'info'">{{"昨收 "+result["昨日收盘价"]}}</n-text>
@@ -338,12 +456,16 @@ function SendMessage(result){
</n-gi>
</n-grid>
<template #header-extra>
<!-- <n-tag size="small" v-if="result.volume>0" :type="result.profitType">{{result.volume+""}}</n-tag>-->
<n-button size="tiny" secondary type="primary" @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
取消关注
</n-button>&nbsp;
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])"> AI分析 </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.profit+"%"+" ( "+result.profitAmount+" ¥ )"}}</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>
@@ -353,37 +475,40 @@ function SendMessage(result){
<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-button size="tiny" type="info" @click="SendMessage(result)"> 钉钉 </n-button>-->
</n-flex>
</template>
</n-card >
</n-gi>
</n-grid>
<n-affix :trigger-bottom="60" v-if="addBTN">
<div style="position: fixed;bottom: 18px;right:0;z-index: 10;width: 350px">
<!-- <n-card :bordered="false">-->
<n-input-group>
<n-button type="info" @click="addBTN=false" >隐藏</n-button>
<n-auto-complete v-model:value="data.name"
<!-- <n-button type="error" @click="addBTN=!addBTN" > <n-icon :component="Search"/>&nbsp;<n-text v-if="addBTN">隐藏</n-text></n-button>-->
<n-auto-complete v-model:value="data.name" v-if="addBTN"
:input-props="{
autocomplete: 'disabled',
}"
:options="options"
placeholder="请输入股票/指数名称或者代码"
clearable @update-value="getStockList" :on-select="onSelect"/>
<n-button type="primary" @click="AddStock">
<n-button type="primary" @click="AddStock" v-if="addBTN">
<n-icon :component="Add"/> &nbsp;关注该股票
</n-button>
</n-input-group>
<!-- </n-card>-->
</n-affix>
</div>
<n-modal transform-origin="center" size="small" v-model:show="modalShow" :title="formModel.name" style="width: 400px" :preset="'card'">
<n-form :model="formModel" :rules="{ costPrice: { required: true, message: '请输入成本'}, volume: { required: true, message: '请输入数量'},alarm:{required: true, message: '涨跌报警值'} }" label-placement="left" label-width="80px">
<n-form :model="formModel" :rules="{
costPrice: { required: true, message: '请输入成本'},
volume: { required: true, message: '请输入数量'},
alarm:{required: true, message: '涨跌报警值'} ,
alarmPrice: { required: true, message: '请输入报警价格'},
sort: { required: true, message: '请输入排序值'},
}" label-placement="left" label-width="80px">
<n-form-item label="股票成本" path="costPrice">
<n-input-number v-model:value="formModel.costPrice" min="0" placeholder="请输入股票成本" >
<template #suffix>
¥
</template>
</n-input-number>
</n-form-item>
@@ -401,9 +526,20 @@ function SendMessage(result){
</template>
</n-input-number>
</n-form-item>
<n-form-item label="股价提醒" path="alarmPrice">
<n-input-number v-model:value="formModel.alarmPrice" min="0" placeholder="请输入股价报警值(¥)" >
<template #suffix>
¥
</template>
</n-input-number>
</n-form-item>
<n-form-item label="股票排序" path="sort">
<n-input-number v-model:value="formModel.sort" min="0" placeholder="请输入股价排序值" >
</n-input-number>
</n-form-item>
</n-form>
<template #footer>
<n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume,formModel.alarm)">保存</n-button>
<n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume,formModel.alarm,formModel)">保存</n-button>
</template>
</n-modal>
@@ -413,28 +549,14 @@ function SendMessage(result){
<n-modal v-model:show="modalShow3" :title="data.name" style="width: 600px" :preset="'card'">
<n-image :src="data.kURL" />
</n-modal>
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;height: 480px" :title="'['+data.name+']AI分析结果'" >
<n-spin size="small" :show="data.loading">
<MdPreview ref="mdPreviewRef" style="height: 380px" :modelValue="data.airesult" :theme="'dark'"/>
</n-spin>
</n-modal>
</template>
<style scoped>
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
}
.input-box {
text-align: center;
}
.input {
width: 200px;
margin-right: 10px;
}
.light-green {
height: 108px;
background-color: rgba(0, 128, 0, 0.12);
}
.green {
height: 108px;
background-color: rgba(0, 128, 0, 0.24);
}
</style>

View File

@@ -1,10 +1,11 @@
import {createApp} from 'vue'
import naive from 'naive-ui'
import App from './App.vue'
import router from './router/router'
const app = createApp(App)
app.use(router)
app.use(naive)
app.mount('#app')

View File

@@ -0,0 +1,16 @@
import { createMemoryHistory, createRouter } from 'vue-router'
import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue'
const routes = [
{ path: '/', component: stockView,name: 'stock' },
{ path: '/settings/:id', component: settingsView,name: 'settings' },
]
const router = createRouter({
history: createMemoryHistory(),
routes,
})
export default router

View File

@@ -4,16 +4,28 @@ import {data} from '../models';
export function Follow(arg1:string):Promise<string>;
export function GetConfig():Promise<data.Settings>;
export function GetFollowList():Promise<Array<data.FollowedStock>>;
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
export function Greet(arg1:string):Promise<data.StockInfo>;
export function NewChat(arg1:string):Promise<string>;
export function NewChatStream(arg1:string,arg2:string):Promise<void>;
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
export function SetAlarmChangePercent(arg1:number,arg2:string):Promise<string>;
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;
export function SetAlarmChangePercent(arg1:number,arg2:number,arg3:string):Promise<string>;
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
export function SetStockSort(arg1:number,arg2:string):Promise<void>;
export function UnFollow(arg1:string):Promise<string>;
export function UpdateConfig(arg1:data.Settings):Promise<string>;

View File

@@ -6,6 +6,10 @@ export function Follow(arg1) {
return window['go']['main']['App']['Follow'](arg1);
}
export function GetConfig() {
return window['go']['main']['App']['GetConfig']();
}
export function GetFollowList() {
return window['go']['main']['App']['GetFollowList']();
}
@@ -18,18 +22,38 @@ export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function NewChat(arg1) {
return window['go']['main']['App']['NewChat'](arg1);
}
export function NewChatStream(arg1, arg2) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2);
}
export function SendDingDingMessage(arg1, arg2) {
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
}
export function SetAlarmChangePercent(arg1, arg2) {
return window['go']['main']['App']['SetAlarmChangePercent'](arg1, arg2);
export function SendDingDingMessageByType(arg1, arg2, arg3) {
return window['go']['main']['App']['SendDingDingMessageByType'](arg1, arg2, arg3);
}
export function SetAlarmChangePercent(arg1, arg2, arg3) {
return window['go']['main']['App']['SetAlarmChangePercent'](arg1, arg2, arg3);
}
export function SetCostPriceAndVolume(arg1, arg2, arg3) {
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
}
export function SetStockSort(arg1, arg2) {
return window['go']['main']['App']['SetStockSort'](arg1, arg2);
}
export function UnFollow(arg1) {
return window['go']['main']['App']['UnFollow'](arg1);
}
export function UpdateConfig(arg1) {
return window['go']['main']['App']['UpdateConfig'](arg1);
}

View File

@@ -9,6 +9,7 @@ export namespace data {
PriceChange: number;
ChangePercent: number;
AlarmChangePercent: number;
AlarmPrice: number;
// Go type: time
Time: any;
Sort: number;
@@ -28,6 +29,7 @@ export namespace data {
this.PriceChange = source["PriceChange"];
this.ChangePercent = source["ChangePercent"];
this.AlarmChangePercent = source["AlarmChangePercent"];
this.AlarmPrice = source["AlarmPrice"];
this.Time = this.convertValues(source["Time"], null);
this.Sort = source["Sort"];
this.IsDel = source["IsDel"];
@@ -51,6 +53,71 @@ export namespace data {
return a;
}
}
export class Settings {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
tushareToken: string;
localPushEnable: boolean;
dingPushEnable: boolean;
dingRobot: string;
updateBasicInfoOnStart: boolean;
refreshInterval: number;
openAiEnable: boolean;
openAiBaseUrl: string;
openAiApiKey: string;
openAiModelName: string;
openAiMaxTokens: number;
openAiTemperature: number;
prompt: string;
static createFrom(source: any = {}) {
return new Settings(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.tushareToken = source["tushareToken"];
this.localPushEnable = source["localPushEnable"];
this.dingPushEnable = source["dingPushEnable"];
this.dingRobot = source["dingRobot"];
this.updateBasicInfoOnStart = source["updateBasicInfoOnStart"];
this.refreshInterval = source["refreshInterval"];
this.openAiEnable = source["openAiEnable"];
this.openAiBaseUrl = source["openAiBaseUrl"];
this.openAiApiKey = source["openAiApiKey"];
this.openAiModelName = source["openAiModelName"];
this.openAiMaxTokens = source["openAiMaxTokens"];
this.openAiTemperature = source["openAiTemperature"];
this.prompt = source["prompt"];
}
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 StockBasic {
ID: number;
// Go type: time
@@ -136,13 +203,14 @@ export namespace data {
"时间": string;
"股票代码": string;
"股票名称": string;
"上次当前价格": number;
"当前价格": string;
"成交的股票数": string;
"成交金额": string;
"今日开盘价": string;
"昨日收盘价": string;
"今日最低价": string;
"今日最高价": string;
"今日最低价": string;
"竞买价": string;
"竞卖价": string;
"买一报价": string;
@@ -165,6 +233,18 @@ export namespace data {
"卖四申报": string;
"卖五报价": string;
"卖五申报": string;
changePercent: number;
changePrice: number;
highRate: number;
lowRate: number;
costPrice: number;
costVolume: number;
profit: number;
profitAmount: number;
profitAmountToday: number;
sort: number;
alarmChangePercent: number;
alarmPrice: number;
static createFrom(source: any = {}) {
return new StockInfo(source);
@@ -180,13 +260,14 @@ export namespace data {
this["时间"] = source["时间"];
this["股票代码"] = source["股票代码"];
this["股票名称"] = source["股票名称"];
this["上次当前价格"] = source["上次当前价格"];
this["当前价格"] = source["当前价格"];
this["成交的股票数"] = source["成交的股票数"];
this["成交金额"] = source["成交金额"];
this["今日开盘价"] = source["今日开盘价"];
this["昨日收盘价"] = source["昨日收盘价"];
this["今日最低价"] = source["今日最低价"];
this["今日最高价"] = source["今日最高价"];
this["今日最低价"] = source["今日最低价"];
this["竞买价"] = source["竞买价"];
this["竞卖价"] = source["竞卖价"];
this["买一报价"] = source["买一报价"];
@@ -209,6 +290,18 @@ export namespace data {
this["卖四申报"] = source["卖四申报"];
this["卖五报价"] = source["卖五报价"];
this["卖五申报"] = source["卖五申报"];
this.changePercent = source["changePercent"];
this.changePrice = source["changePrice"];
this.highRate = source["highRate"];
this.lowRate = source["lowRate"];
this.costPrice = source["costPrice"];
this.costVolume = source["costVolume"];
this.profit = source["profit"];
this.profitAmount = source["profitAmount"];
this.profitAmountToday = source["profitAmountToday"];
this.sort = source["sort"];
this.alarmChangePercent = source["alarmChangePercent"];
this.alarmPrice = source["alarmPrice"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {

22
go.mod
View File

@@ -1,18 +1,21 @@
module go-stock
go 1.21
go 1.23
toolchain go1.23.0
require (
github.com/PuerkitoBio/goquery v1.10.1
github.com/chromedp/chromedp v0.11.2
github.com/coocood/freecache v1.2.4
github.com/duke-git/lancet/v2 v2.3.4
github.com/getlantern/systray v1.2.2
github.com/glebarez/sqlite v1.11.0
github.com/go-resty/resty/v2 v2.16.2
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/wailsapp/wails/v2 v2.9.2
go.uber.org/zap v1.27.0
golang.org/x/text v0.16.0
golang.org/x/text v0.21.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3
@@ -20,8 +23,11 @@ require (
)
require (
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
@@ -32,19 +38,25 @@ require (
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/echo/v4 v4.10.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
github.com/leaanthony/gosod v1.0.3 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -57,10 +69,10 @@ require (
github.com/wailsapp/go-webview2 v1.0.16 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect

74
go.sum
View File

@@ -1,7 +1,17 @@
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb h1:noKVm2SsG4v0Yd0lHNtFYc9EUxIVvrr4kJ6hM8wvIYU=
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
github.com/chromedp/chromedp v0.11.2 h1:ZRHTh7DjbNTlfIv3NFTbB7eVeu5XCNkgrpcGSpn2oX0=
github.com/chromedp/chromedp v0.11.2/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -37,8 +47,17 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -51,6 +70,8 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
@@ -66,8 +87,12 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -80,6 +105,10 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
@@ -124,22 +153,36 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -157,25 +200,40 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

81
main.go
View File

@@ -16,6 +16,7 @@ import (
"go-stock/backend/db"
"log"
"os"
goruntime "runtime"
"time"
)
@@ -39,12 +40,12 @@ func main() {
db.Dao.AutoMigrate(&data.StockBasic{})
db.Dao.AutoMigrate(&data.FollowedStock{})
db.Dao.AutoMigrate(&data.IndexBasic{})
db.Dao.AutoMigrate(&data.Settings{})
if stocksBin != nil && len(stocksBin) > 0 {
go initStockData()
}
go data.NewStockDataApi().GetStockBaseInfo()
go data.NewStockDataApi().GetIndexBasic()
updateBasicInfo()
// Create an instance of the app structure
app := NewApp()
@@ -63,42 +64,51 @@ func main() {
FileMenu.AddSeparator()
FileMenu.AddText("窗口全屏", keys.CmdOrCtrl("f"), func(callback *menu.CallbackData) {
runtime.WindowFullscreen(app.ctx)
callback.MenuItem.Hide()
})
FileMenu.AddText("窗口还原", keys.Key("Esc"), func(callback *menu.CallbackData) {
runtime.WindowUnfullscreen(app.ctx)
})
FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("h"), func(_ *menu.CallbackData) {
runtime.Hide(app.ctx)
})
FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
runtime.Quit(app.ctx)
})
if goruntime.GOOS == "windows" {
FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("h"), func(_ *menu.CallbackData) {
runtime.WindowHide(app.ctx)
})
FileMenu.AddText("显示", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {
runtime.WindowShow(app.ctx)
})
}
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
// runtime.Quit(app.ctx)
//})
// Create application with options
err := wails.Run(&options.App{
Title: "go-stock",
Width: 1366,
Height: 860,
MinWidth: 1024,
MinHeight: 768,
MaxWidth: 1280,
MaxHeight: 960,
DisableResize: false,
Fullscreen: false,
Frameless: false,
StartHidden: false,
HideWindowOnClose: false,
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
Assets: assets,
Menu: AppMenu,
Logger: nil,
LogLevel: logger.DEBUG,
OnStartup: app.startup,
OnDomReady: app.domReady,
OnBeforeClose: app.beforeClose,
OnShutdown: app.shutdown,
WindowStartState: options.Normal,
Title: "go-stock",
Width: 1366,
Height: 920,
MinWidth: 1024,
MinHeight: 768,
MaxWidth: 1920,
MaxHeight: 960,
DisableResize: false,
Fullscreen: false,
Frameless: true,
StartHidden: false,
HideWindowOnClose: false,
EnableDefaultContextMenu: true,
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
Assets: assets,
Menu: AppMenu,
Logger: nil,
LogLevel: logger.DEBUG,
LogLevelProduction: logger.ERROR,
OnStartup: app.startup,
OnDomReady: app.domReady,
OnBeforeClose: app.beforeClose,
OnShutdown: app.shutdown,
WindowStartState: options.Normal,
Bind: []interface{}{
app,
},
@@ -137,6 +147,15 @@ func main() {
}
func updateBasicInfo() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
if config.UpdateBasicInfoOnStart {
//更新基本信息
go data.NewStockDataApi().GetStockBaseInfo()
go data.NewStockDataApi().GetIndexBasic()
}
}
func initStockData() {
var count int64
db.Dao.Model(&data.StockBasic{}).Count(&count)

View File

@@ -7,13 +7,13 @@
"frontend:dev:serverUrl": "http://localhost:5173",
"author": {
"name": "spark",
"email": "wzl@huazx.cn"
"email": "sparkmemory@163.com"
},
"info": {
"companyName": "sparkmemory",
"productName": "go-stock",
"productVersion": "1.0.0",
"copyright": "Copyright#sparkmemory@163.com",
"comments": "股票行情实时获取"
"comments": "股票行情实时获取,AI赋能分析股票"
}
}