Compare commits

...

124 Commits

Author SHA1 Message Date
spark
d5db2ef879 feat(backend): 添加获取财务报告功能并优化聊天流
- 新增 GetFinancialReports 函数,用于抓取股票财务报告信息
- 优化 NewChatStream 函数,增加财务报告信息到聊天流中
- 更新测试用例,使用北京文化(sz000802)作为示例股票- 添加 TestGetFinancialReports 和 TestXUEQIU 测试函数
2025-02-05 16:25:24 +08:00
spark
509cd2dbca refactor(backend): 调整 openai_api.go 中的资源关闭逻辑
- 将 resp.RawBody().Close() 调用移动到 if err != nil块之后
- 确保在发生错误时也能正确关闭网络连接
- 优化了代码结构,提高了资源管理的可靠性
2025-02-04 20:02:21 +08:00
spark
3de2ad3cdc refactor(backend): 重构 OpenAI 和股票数据 API
-优化了 OpenAI API 的调用逻辑,提高了错误处理和数据处理的能力
- 改进了股票数据 API 的数据抓取和处理方式
- 移除了测试代码中冗余的部分,提高了代码可读性和维护性
2025-02-04 19:45:22 +08:00
spark
b00bddcdec refactor(stock-data): 重构股票数据获取逻辑
- 移除了不必要的并发请求,简化了代码结构
- 新增 FetchPrice 函数,用于获取股票价格信息
- 优化 SearchStockInfo 函数,提高了搜索效率和准确性
- 新增 SearchStockInfoByCode 函数,用于根据股票代码获取相关信息- 修复了一些潜在的错误和性能问题
2025-02-04 18:12:08 +08:00
spark
64b37b687c refactor(data): 优化 OpenAI API 客户端配置并改进流数据处理
- 将请求超时时间从 30秒增加到 60 秒
- 修正流数据的前缀检查,从 "chat data: " 改为 "data: "- 增加对 reasoning_content 的处理逻辑
- 优化数据处理流程,提高错误处理能力
2025-02-04 15:12:15 +08:00
spark
e81319bb4f docs(README): 添加赞助信息在 README.md 中添加了赞助信息部分,提供了支付宝和微信支付的二维码图片链接,鼓励对项目有帮助的用户进行赞助。 2025-02-04 07:31:44 +08:00
spark
7bc219d1a5 refactor(frontend): 优化 OpenAI 设置界面文案
- 将"自定义Prompt"标签修改为"自定义系统Prompt"
- 更新输入框占位符为"请输入系统prompt"
2025-02-03 22:03:16 +08:00
spark
0f2f58e6b8 docs: 更新 README 中的设置截图
- 将 README.md 中的 img_11.png 替换为 img_12.png
- 优化设置界面的视觉效果
2025-02-03 21:53:31 +08:00
spark
2dc0b95b45 docs: 更新 README 中的设置截图
- 将 README.md 中的 img_11.png 替换为 img_12.png
- 优化设置界面的视觉效果
2025-02-03 14:01:20 +08:00
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
sparkmemory
04446d7521 refactor(app): 优化菜单项创建顺序和股票收益计算逻辑- 调整 systray 菜单项创建顺序,将"退出"菜单项放在最后- 修正股票收益计算逻辑,确保正确处理负数情况 2025-01-04 19:11:58 +08:00
spark
2306a8e225 ui(systray): 修改系统托盘菜单项名称
-将"隐藏应用程序"菜单项修改为"隐藏"
- 优化菜单项名称,使其更加简洁明了
2025-01-04 14:42:55 +08:00
sparkmemory
46aff404d4 update 2025-01-03 23:19:57 +08:00
sparkmemory
1e5d9bc469 Merge branch 'master' of https://github.com/ArvinLovegood/go-stock
# Conflicts:
#	stock_basic.json
2025-01-03 23:03:40 +08:00
spark
98844ce717 feat(app): 添加系统托盘功能
- 使用 systray 库创建系统托盘图标和菜单- 添加退出、显示和隐藏应用程序的菜单项
- 实现托盘图标初始化和清理逻辑
- 更新 go.mod 和 go.sum 文件,添加相关依赖
2025-01-03 22:54:39 +08:00
spark
2bd91c6555 refactor(frontend): 调整股票数量输入的最小步长为 100
- 在 Stock 组件中的股票数量输入框中添加 step 属性
-将 step 属性设置为 100,以符合业务需求
2025-01-03 17:02:35 +08:00
spark
2166b0a39b feat(stock): 优化股票对话框输入项
- 为成本、数量和涨跌报警值添加单位后缀
- 优化表单项的标签文案
- 调整输入框样式,增加单位后缀
2025-01-03 16:58:47 +08:00
spark
2f2b19f5d7 feat(app): 添加钉钉消息发送功能和股票涨跌报警
- 新增 SendDingDingMessage 和 SetAlarmChangePercent 函数- 实现钉钉消息发送和股票涨跌报警逻辑
- 更新前端界面,增加报警值设置和消息发送功能
- 新增 DingDingAPI 结构体和相关方法
2025-01-03 16:43:32 +08:00
spark
685a7d23b2 feat(stock_data_api): 搜索股票时包含深交所指数
- 将指数市场范围从上交所(SSE)扩展到包括深交所(SZSE)
- 优化了股票和指数的搜索逻辑,提高搜索结果的全面性
2025-01-03 13:17:19 +08:00
spark
5f1eaf02c4 feat(frontend): 扩展股票搜索框占位符文本
- 将搜索框的占位符文本从"请输入股票名称或者代码"修改为"请输入股票/指数名称或者代码"
- 这个修改使得用户更加清晰地知道可以在搜索框中输入股票或指数的名称或代码
2025-01-03 13:05:43 +08:00
spark
116dae19cf feat(stock): 搜索股票时增加指数匹配
- 在搜索股票时增加对上交所指数的匹配
- 优化股票代码输入逻辑,增加空值判断
-调整关注股票功能,避免重复关注
- 修改分时图数据的更新频率为 3.5 秒一次
2025-01-03 13:04:29 +08:00
spark
a35b42f831 refactor(backend): 移除股票数据 API 中的冗余代码
- 删除了 GetIndexBasic 和 GetStockBaseInfo 方法中的冗余代码
- 移除了不必要的文件写入操作和注释掉的代码
- 优化了代码结构,提高了代码的可读性和维护性
2025-01-03 09:57:19 +08:00
spark
513cd69e3e build: 更新 .gitignore 文件以忽略构建输出
- 修改 .gitignore 文件,更新对构建输出文件和目录的忽略规则
-保留了对 .idea 目录和 data/*.db 文件的忽略- 更新了对 build 目录下可执行文件的忽略规则
2025-01-03 09:54:27 +08:00
spark
0ce01bcdf0 build: 更新 .gitignore 文件以忽略构建输出
- 修改 .gitignore 文件,更新对构建输出文件和目录的忽略规则
-保留了对 .idea 目录和 data/*.db 文件的忽略- 更新了对 build 目录下可执行文件的忽略规则
2025-01-03 09:51:45 +08:00
spark
afe5474264 refactor(stock): 优化股票组件和数据更新逻辑
- 修改股票列表显示格式,将代码和名称之间的连接符改为短横线- 调整股票数据更新频率,从 3 秒改为 1 秒
- 修复当前价格为零时的显示问题,使用卖一报价替代
- 优化数据库更新操作,添加 ts_code 条件以确保更新正确性
2025-01-03 09:48:21 +08:00
spark
f35847823b feat(data): 添加指数信息获取功能
- 在 StockDataApi 中新增 GetIndexBasic 方法,用于获取指数信息
- 在数据库中添加 index_basic 表并进行自动迁移- 优化 GetStockBaseInfo 方法,使用 map 结构处理字段- 增加 GetIndexBasic 的单元测试
2025-01-02 17:52:25 +08:00
spark
15120c98da feat(frontend): 优化股票代码输入和搜索功能
- 改进股票列表显示格式,增加连字符分隔
- 添加支持直接输入股票代码进行搜索的功能
- 优化股票选择逻辑,支持不同格式的股票代码
-增加调试日志输出,便于问题排查
2025-01-02 15:39:39 +08:00
sparkmemory
6903abd6f8 update 2024-12-29 07:50:12 +08:00
spark
f9a0c8d94e 样式修改,新增菜单工具栏 2024-12-28 14:41:45 +08:00
spark
0c04272153 样式修改,新增菜单工具栏 2024-12-25 13:13:23 +08:00
sparkmemory
cf7e8415e6 update 2024-12-23 11:26:24 +08:00
sparkmemory
1ab6875790 update 2024-12-23 11:25:33 +08:00
sparkmemory
cc40da8371 update 2024-12-23 11:21:03 +08:00
sparkmemory
29158f51af update 2024-12-23 07:34:58 +08:00
spark
9665462ae5 样式修改 2024-12-20 21:02:37 +08:00
spark
71e3953cd8 添加缩略图 2024-12-20 15:06:09 +08:00
spark
bedeaad1f2 添加按钮位置修改 2024-12-20 14:37:38 +08:00
spark
4f05820e2e 优化首次启动速度 2024-12-20 14:34:24 +08:00
spark
abf05aabd8 退出程序添加提示 2024-12-20 14:33:54 +08:00
spark
9016289e96 样式调整 2024-12-20 11:07:46 +08:00
spark
bab53711b7 添加全屏按钮 2024-12-20 11:01:56 +08:00
spark
de67f07f41 修改为深色主题 2024-12-20 10:01:56 +08:00
spark
869223cfb9 修改为深色主题 2024-12-20 10:00:46 +08:00
spark
d240239fcc 窗口标题显示当前时间 2024-12-19 20:59:01 +08:00
spark
6b62385cad 分时图和K线图展示 2024-12-19 13:48:02 +08:00
48 changed files with 3862 additions and 309 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 }}

7
.gitignore vendored
View File

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

View File

@@ -1,11 +1,33 @@
# README
![Wails and NaiveUI](./build/appicon.png)
## 自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具
经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
## BIG NEWS !!! 重大更新!!!
- 2025.01.17 新增AI大模型分析股票功能
![img_5.png](build/screenshot/img_10.png)
## 简介
- 本项目基于Wails和NaiveUI纯属无聊仅供娱乐不喜勿喷。
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
## Snapshot
![img_1.png](build/screenshot/img_6.png)
### 设置
![img_12.png](build/screenshot/img_12.png)
### 成本设置
![img.png](build/screenshot/img_7.png)
### 日K
![img_2.png](build/screenshot/img_8.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
@@ -28,4 +50,9 @@ 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/)
## 都划到这了,如果我的项目对您有帮助,请赞助我吧!😊😊😊
| 支付宝 | 微信 |
|-----|-----|
| ![alipay.jpg](build/screenshot/alipay.jpg) | ![wxpay.jpg](build/screenshot/wxpay.jpg) |

426
app.go
View File

@@ -1,46 +1,319 @@
//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
type App struct {
ctx context.Context
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
return &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
// 创建系统托盘
systray.Run(func() {
go onReady(a)
}, func() {
go onExit(a)
})
}
// domReady is called after front-end resources have been loaded
func (a App) domReady(ctx context.Context) {
func (a *App) domReady(ctx context.Context) {
// Add your action here
//定时更新数据
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,
// 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 {
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
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
systray.Quit()
}
// 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
}
@@ -63,3 +336,146 @@ func (a *App) GetStockList(key string) []data.StockBasic {
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) 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)
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)
}
// 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")
runtime.Quit(a.ctx)
}
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("隐藏", "隐藏应用程序")
systray.AddSeparator()
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
// 监听菜单项点击事件
go func() {
for {
select {
case <-mQuitOrig.ClickedCh:
logger.SugaredLogger.Infof("退出应用程序")
runtime.Quit(a.ctx)
//systray.Quit()
case <-show.ClickedCh:
logger.SugaredLogger.Infof("显示应用程序")
runtime.WindowShow(a.ctx)
//runtime.WindowShow(a.ctx)
case <-hide.ClickedCh:
logger.SugaredLogger.Infof("隐藏应用程序")
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

@@ -0,0 +1,85 @@
package data
import (
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
)
// @Author spark
// @Date 2025/1/3 13:53
// @Desc
//-----------------------------------------------------------------------------------
type DingDingAPI struct {
client *resty.Client
}
func NewDingDingAPI() *DingDingAPI {
return &DingDingAPI{
client: resty.New(),
}
}
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(getApiURL())
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "发送钉钉消息失败"
}
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 {
// 发送钉钉消息
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(&Message{
Msgtype: "markdown",
Markdown: Markdown{
Title: "go-stock " + title,
Text: message,
},
At: At{
IsAtAll: true,
},
}).
Post(getApiURL())
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "发送钉钉消息失败"
}
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
return "发送钉钉消息成功"
}
type Message struct {
Msgtype string `json:"msgtype"`
Markdown Markdown `json:"markdown"`
At At `json:"at"`
}
type Markdown struct {
Title string `json:"title"`
Text string `json:"text"`
}
type At struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
}

View File

@@ -0,0 +1,32 @@
package data
import (
"github.com/go-resty/resty/v2"
"testing"
)
// @Author spark
// @Date 2025/1/3 13:53
// @Desc
//-----------------------------------------------------------------------------------
func TestRobot(t *testing.T) {
dingdingRobotUrl := "XXX"
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(`{
"msgtype": "markdown",
"markdown": {
"title":"go-stock",
"text": "#### 杭州天气 @150XXXXXXXX \n > 9度西北风1级空气良89相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n"
},
"at": {
"isAtAll": true
}
}`).
Post(dingdingRobotUrl)
if err != nil {
t.Error(err)
}
t.Log(resp.String())
}

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

@@ -0,0 +1,446 @@
package data
import (
"bufio"
"context"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/strutil"
"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, 512)
go func() {
defer close(ch)
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(5)
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 := GetFinancialReports(stockCode)
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + message,
})
}
}()
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 + "分析和总结",
})
client := resty.New()
client.SetBaseURL(o.BaseUrl)
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3)
client.SetTimeout(1 * time.Minute)
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")
defer resp.RawBody().Close()
if err != nil {
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
ch <- err.Error()
return
}
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"`
ReasoningContent string `json:"reasoning_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
logger.SugaredLogger.Infof("Content data: %s", content)
}
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
ch <- reasoningContent
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
}
if choice.FinishReason == "stop" {
return
}
}
} else {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
ch <- err.Error()
}
} else {
ch <- line
}
}
}()
return ch
}
func GetFinancialReports(stockCode 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://xueqiu.com/snowman/S/%s/detail#/ZYCWZB", stockCode)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
chromedp.WaitVisible("table.table", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
var messages []string
document.Find("table tr").Each(func(i int, selection *goquery.Selection) {
tr := ""
selection.Find("th,td").Each(func(i int, selection *goquery.Selection) {
ret := selection.Find("p").First().Text()
if ret == "" {
ret = selection.Text()
}
text := strutil.RemoveNonPrintable(ret)
tr += text + " "
})
logger.SugaredLogger.Infof("%s", tr+" \n")
messages = append(messages, tr+" \n")
})
return &messages
}
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("北京文化", "sz000802")
for {
select {
case msg := <-res:
if msg == "" {
continue
}
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,64 +6,85 @@ 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"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"gorm.io/gorm"
"io/ioutil"
"gorm.io/plugin/soft_delete"
"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 {
@@ -125,15 +146,18 @@ type StockBasic struct {
}
type FollowedStock struct {
StockCode string
Name string
Volume int64
CostPrice float64
Price float64
PriceChange float64
ChangePercent float64
Time time.Time
Sort int64
StockCode string
Name string
Volume int64
CostPrice float64
Price float64
PriceChange float64
ChangePercent float64
AlarmChangePercent float64
AlarmPrice float64
Time time.Time
Sort int64
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver FollowedStock) TableName() string {
@@ -158,20 +182,68 @@ func (receiver StockBasic) TableName() string {
func NewStockDataApi() *StockDataApi {
return &StockDataApi{
client: resty.New(),
config: getConfig(),
}
}
// GetIndexBasic 获取指数信息
func (receiver StockDataApi) GetIndexBasic() {
res := &TushareStockBasicResponse{}
fields := "ts_code,name,market,publisher,category,base_date,base_point,list_date,fullname,index_type,weight_rule,desc"
_, err := receiver.client.R().
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: "index_basic",
Token: receiver.config.TushareToken,
Params: nil,
Fields: fields}).
SetResult(res).
Post(tushareApiUrl)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
if res.Code != 0 {
logger.SugaredLogger.Error(res.Msg)
return
}
//ioutil.WriteFile("index_basic.json", resp.Body(), 0666)
for _, item := range res.Data.Items {
data := map[string]any{}
for _, field := range strings.Split(fields, ",") {
idx := slice.IndexOf(res.Data.Fields, field)
if idx == -1 {
continue
}
data[field] = item[idx]
}
index := &IndexBasic{}
jsonData, _ := json.Marshal(data)
err := json.Unmarshal(jsonData, index)
if err != nil {
continue
}
db.Dao.Model(&IndexBasic{}).FirstOrCreate(index, &IndexBasic{TsCode: index.TsCode}).Where("ts_code = ?", index.TsCode).Updates(index)
}
}
// map转换为结构体
func (receiver StockDataApi) GetStockBaseInfo() {
res := &TushareStockBasicResponse{}
fields := "ts_code,symbol,name,area,industry,cnspell,market,list_date,act_name,act_ent_type,fullname,exchange,list_status,curr_type,enname,delist_date,is_hs"
_, err := receiver.client.R().
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: "stock_basic",
Token: TushareToken,
Token: receiver.config.TushareToken,
Params: nil,
Fields: "*",
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)
@@ -185,65 +257,83 @@ func (receiver StockDataApi) GetStockBaseInfo() {
return
}
for _, item := range res.Data.Items {
ID, _ := convertor.ToInt(item[6])
stock := &StockBasic{}
stock.Exchange = convertor.ToString(item[0])
stock.IsHs = convertor.ToString(item[1])
stock.Name = convertor.ToString(item[2])
stock.Industry = convertor.ToString(item[3])
stock.ListStatus = convertor.ToString(item[4])
stock.ActName = convertor.ToString(item[5])
stock.ID = uint(ID)
stock.CurrType = convertor.ToString(item[7])
stock.Area = convertor.ToString(item[8])
stock.ListDate = convertor.ToString(item[9])
stock.DelistDate = convertor.ToString(item[10])
stock.ActEntType = convertor.ToString(item[11])
stock.TsCode = convertor.ToString(item[12])
stock.Symbol = convertor.ToString(item[13])
stock.Cnspell = convertor.ToString(item[14])
stock.Fullname = convertor.ToString(item[20])
stock.Ename = convertor.ToString(item[21])
db.Dao.Model(&StockBasic{}).FirstOrCreate(stock, &StockBasic{TsCode: stock.TsCode}).Updates(stock)
data := map[string]any{}
for _, field := range strings.Split(fields, ",") {
logger.SugaredLogger.Infof("field: %s", field)
idx := slice.IndexOf(res.Data.Fields, field)
if idx == -1 {
continue
}
data[field] = item[idx]
}
jsonData, _ := json.Marshal(data)
err := json.Unmarshal(jsonData, stock)
if err != nil {
continue
}
db.Dao.Model(&StockBasic{}).FirstOrCreate(stock, &StockBasic{TsCode: stock.TsCode}).Where("ts_code = ?", stock.TsCode).Updates(stock)
}
}
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 {
stockInfo, err := receiver.GetStockCodeRealTimeData(stockCode)
logger.SugaredLogger.Infof("Follow %s", 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 "关注成功"
}
@@ -262,6 +352,22 @@ func (receiver StockDataApi) SetCostPriceAndVolume(price float64, volume int64,
return "设置成功"
}
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 "设置失败"
}
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)
@@ -271,13 +377,27 @@ func (receiver StockDataApi) GetFollowList() []FollowedStock {
func (receiver StockDataApi) GetStockList(key string) []StockBasic {
var result []StockBasic
db.Dao.Model(&StockBasic{}).Where("name like ? or ts_code like ?", "%"+key+"%", "%"+key+"%").Find(&result)
var result2 []IndexBasic
db.Dao.Model(&IndexBasic{}).Where("market in ?", []string{"SSE", "SZSE"}).Where("name like ? or ts_code like ?", "%"+key+"%", "%"+key+"%").Find(&result2)
for _, item := range result2 {
result = append(result, StockBasic{
TsCode: item.TsCode,
Name: item.Name,
Fullname: item.FullName,
Symbol: item.Symbol,
Market: item.Market,
ListDate: item.ListDate,
})
}
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)
}
@@ -369,3 +489,161 @@ func ParseFullSingleStockData(data string) (*StockInfo, error) {
return stockInfo, nil
}
type IndexBasic struct {
gorm.Model
TsCode string `json:"ts_code" gorm:"index"`
Symbol string `json:"symbol" gorm:"index"`
Name string `json:"name" gorm:"index"`
FullName string `json:"fullname"`
IndexType string `json:"index_type"`
IndexCategory string `json:"category"`
Market string `json:"market"`
ListDate string `json:"list_date"`
BaseDate string `json:"base_date"`
BasePoint float64 `json:"base_point"`
Publisher string `json:"publisher"`
WeightRule string `json:"weight_rule"`
DESC string `json:"desc"`
}
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 {
price, _ := FetchPrice(ctx)
logger.SugaredLogger.Infof("price:%s", price)
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 FetchPrice(ctx context.Context) (string, error) {
var price string
timeout := time.After(10 * time.Second) // 设置超时时间为10秒
ticker := time.NewTicker(1 * time.Second) // 每秒尝试一次
defer ticker.Stop()
for {
select {
case <-timeout:
return "", fmt.Errorf("timeout reached while fetching price")
case <-ticker.C:
err := chromedp.Run(ctx, chromedp.Text("span.quote-price", &price, chromedp.BySearch))
if err != nil {
logger.SugaredLogger.Errorf("failed to fetch price: %v", err)
continue
}
logger.SugaredLogger.Infof("price:%s", price)
if price != "" && validator.IsNumberStr(price) {
return price, nil
}
}
}
}
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(".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(".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: %s", msgType, text)
}
})
return &messages
}
func SearchStockInfoByCode(stock string) *[]string {
// 创建一个 chromedp 上下文
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
defer cancel()
var htmlContent string
stock = strings.ReplaceAll(stock, "sh", "")
stock = strings.ReplaceAll(stock, "sz", "")
url := fmt.Sprintf("https://gushitong.baidu.com/stock/ab-%s", stock)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
//chromedp.Sleep(3*time.Second),
chromedp.WaitVisible("a.news-item-link", 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("a.news-item-link").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

@@ -1,12 +1,21 @@
package data
import (
"bufio"
"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/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"io/ioutil"
"strings"
"testing"
"time"
)
// @Author spark
@@ -14,9 +23,165 @@ import (
// @Desc
//-----------------------------------------------------------------------------------
func TestGetTelegraph(t *testing.T) {
GetTelegraphList()
}
func TestGetFinancialReports(t *testing.T) {
GetFinancialReports("sz000802")
}
func TestXUEQIU(t *testing.T) {
stock := "北京文化"
stockCode := "SZ000802"
// 创建一个 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://xueqiu.com/S/%s", stockCode)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
//chromedp.Sleep(3*time.Second),
chromedp.WaitVisible("table.quote-info", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
table := ""
document.Find("table.quote-info tbody td").Each(func(i int, selection *goquery.Selection) {
table += selection.Text() + ";"
})
logger.SugaredLogger.Infof("table: %s", table)
client := resty.New()
client.SetBaseURL("https://api.siliconflow.cn/v1")
client.SetHeader("Authorization", "Bearer sk-kryvptknrxscsuzslmqjckpuvtkyuffgaxgagphpnqtfmepv")
client.SetHeader("Content-Type", "application/json")
client.SetRetryCount(3)
client.SetTimeout(1 * time.Minute)
msg := []map[string]interface{}{
{
"role": "system",
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
"content": "【角色设定】\n你现在是拥有20年实战经验的顶级股票投资大师精通价值投资、趋势交易、量化分析等多种策略。\n擅长结合宏观经济、行业周期和企业基本面进行多维分析尤其对A股、港股、美股市场有深刻理解。\n始终秉持\"风险控制第一\"的原则,善于用通俗易懂的方式传授投资智慧。\n\n【核心能力】\n基本面分析专家\n深度解读财报数据PE/PB/ROE等指标\n识别企业核心竞争力与护城河\n评估行业前景与政策影响\n技术面分析大师\n精准识别K线形态与量价关系\n运用MACD/RSI/布林线等指标判断买卖点\n绘制关键支撑/阻力位\n风险管理专家\n根据风险偏好制定仓位策略\n设置动态止盈止损方案\n设计投资组合对冲方案\n市场心理学导师\n识别主力资金动向\n预判市场情绪周期\n规避常见认知偏差\n【服务范围】\n个股诊断分析提供代码/名称)\n行业趋势解读科技/消费/医疗等)\n投资策略定制长线价值/波段操作/打新等)\n组合优化建议股债配置/行业分散)\n投资心理辅导克服贪婪恐惧\n【交互风格】\n采用\"先结论后分析\"的表达方式\n重要数据用★标注风险提示用❗标记\n每次分析至少提供3个可执行建议"},
}
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": table,
})
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "分析和总结",
})
resp, err := client.R().
SetDoNotParseResponse(true).
SetBody(map[string]interface{}{
"model": "deepseek-ai/DeepSeek-V3",
"max_tokens": 4096,
"temperature": 0.1,
"stream": true,
"messages": msg,
}).
Post("/chat/completions")
defer resp.RawBody().Close()
if err != nil {
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
return
}
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"`
ReasoningContent string `json:"reasoning_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 != "" {
logger.SugaredLogger.Infof("Content data: %s", content)
}
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
}
if choice.FinishReason == "stop" {
return
}
}
} else {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
}
}
}
}
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("闻泰科技", "telegram")
for _, message := range *messages {
logger.SugaredLogger.Info(message)
}
//https://www.cls.cn/stock?code=sh600745
}
func TestSearchStockInfoByCode(t *testing.T) {
SearchStockInfoByCode("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,6 +239,12 @@ func TestReadFile(t *testing.T) {
func TestFollowedList(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
t.Log(stockDataApi.GetFollowList())
stockDataApi.GetFollowList()
}
func TestStockDataApi_GetIndexBasic(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
stockDataApi.GetIndexBasic()
}

BIN
build/app.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

BIN
build/screenshot/alipay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
build/screenshot/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
build/screenshot/img_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
build/screenshot/img_10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
build/screenshot/img_12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
build/screenshot/img_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
build/screenshot/img_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 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

BIN
build/screenshot/wxpay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 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

@@ -8,11 +8,14 @@
"name": "go-stock",
"version": "1.0.0",
"dependencies": {
"vue": "^3.2.25"
"@vicons/ionicons5": "^0.13.0",
"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"
}
@@ -59,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",
@@ -462,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",
@@ -721,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",
@@ -736,6 +1263,30 @@
"@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",
"integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ=="
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
@@ -795,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",
@@ -840,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",
@@ -862,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",
@@ -969,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",
@@ -981,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",
@@ -989,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",
@@ -1068,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",
@@ -1120,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",
@@ -1235,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",
@@ -1252,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

@@ -9,14 +9,17 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.25"
"@vicons/ionicons5": "^0.13.0",
"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 @@
9ce62efac1fed08499bbf20c8a5fd1b2
39a415166f03acc0270e24443a9e2445

View File

@@ -1,32 +1,196 @@
<script setup>
import stockInfo from './components/stock.vue'
import {ref} from "vue";
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" 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,10 +1,25 @@
<script setup>
import {h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
import {Greet, Follow, UnFollow, GetFollowList, GetStockList, SetCostPriceAndVolume} from '../../wailsjs/go/main/App'
import {NButton, NFlex, NForm, NFormItem, NInput, NInputNumber, NText, useMessage, useModal} from 'naive-ui'
import {computed, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
import {
Follow, GetConfig,
GetFollowList,
GetStockList,
Greet, NewChat, NewChatStream,
SendDingDingMessage, SendDingDingMessageByType,
SetAlarmChangePercent,
SetCostPriceAndVolume, SetStockSort,
UnFollow
} from '../../wailsjs/go/main/App'
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({})
@@ -13,26 +28,48 @@ const stockList=ref([])
const followList=ref([])
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: "",
code: "",
costPrice: 0.000,
volume: 0
volume: 0,
alarm: 0,
alarmPrice:0,
sort:999,
})
const data = reactive({
name: "",
code: "",
fenshiURL:"",
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 => {
stockList.value = result
options.value=result.map(item => {
return {
label: item.name+" "+item.ts_code,
label: item.name+" - "+item.ts_code,
value: item.ts_code
}
})
@@ -45,27 +82,73 @@ 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()
}
}, 1000)
}, 3500)
})
onBeforeUnmount(() => {
console.log(`the component is now unmounted.`)
// console.log(`the component is now unmounted.`)
clearInterval(ticker.value)
})
EventsOn("refresh",(data)=>{
message.success(data)
})
EventsOn("showSearch",(data)=>{
addBTN.value = data === 1;
})
EventsOn("stock_price",(data)=>{
//console.log("stock_price",data['股票代码'])
updateData(data)
})
EventsOn("refreshFollowList",(data)=>{
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() {
@@ -93,92 +176,163 @@ function AddStock(){
Follow(data.code).then(result => {
message.success(result)
})
monitor()
}else{
message.error("已经关注了")
}
monitor()
}
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(){
function 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,
label: item.name+" - "+item.ts_code,
value: item.ts_code
}
})
if(value&&value.indexOf("-")<=0){
data.code=value
}
}
function monitor() {
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 => {
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.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"
}
}
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) {
data.code=item.split(".")[1].toLowerCase()+item.split(".")[0]
//console.log("onSelect",item)
if(item.indexOf("-")>0){
item=item.split("-")[1].toLowerCase()
}
if(item.indexOf(".")>0){
data.code=item.split(".")[1].toLowerCase()+item.split(".")[0]
}
}
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
}
function updateCostPriceAndVolumeNew(code,price,volume){
console.log(code,price,volume)
function showFenshi(code,name){
data.code=code
data.name=name
data.fenshiURL='http://image.sinajs.cn/newchart/min/n/'+data.code+'.gif'+"?t="+Date.now()
modalShow2.value=true
}
function showK(code,name){
data.code=code
data.name=name
data.kURL='http://image.sinajs.cn/newchart/daily/n/'+data.code+'.gif'+"?t="+Date.now()
modalShow3.value=true
}
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)
})
}
SetCostPriceAndVolume(code,price,volume).then(result => {
modalShow.value=false
message.success(result)
@@ -190,95 +344,219 @@ function updateCostPriceAndVolumeNew(code,price,volume){
}
}
monitor()
message.destroyAll
message.destroyAll()
})
})
}
function fullscreen(){
if(data.fullscreen){
WindowUnfullscreen()
}else{
WindowFullscreen()
}
data.fullscreen=!data.fullscreen
}
//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 ["+typeName+"]\n\n"+
"### "+result["股票名称"]+"("+result["股票代码"]+")\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.costVolume+"股\n" +
"- 日期: "+result["日期"]+" "+result["时间"]+"\n\n"+
"![image]("+img+")\n"
let title=result["股票名称"]+"("+result["股票代码"]+") "+result["当前价格"]+" "+result.changePercent
let msg='{' +
' "msgtype": "markdown",' +
' "markdown": {' +
' "title":"['+typeName+"]"+title+'",' +
' "text": "'+markdown+'"' +
' },' +
' "at": {' +
' "isAtAll": true' +
' }' +
' }'
// 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 size="medium" style="min-height: 270px" :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :content-style="'font-size: 18px;'" closable @close="removeMonitor(result['股票代码'],result['股票名称'])">
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
<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-gi>
</n-grid>
<n-grid :cols="2" :y-gap="4" :x-gap="4" :item-style="'font-size: 14px;'">
<n-gi>
<n-text :type="'info'">{{"最高 "+result["今日最高价"]+" "+result.highRate }}</n-text>
</n-gi>
<n-gi>
<n-text :type="'info'">{{"最低 "+result["今日最低价"]+" "+result.lowRate }}</n-text>
</n-gi>
<n-gi>
<n-text :type="'info'">{{"昨收 "+result["昨日收盘价"]}}</n-text>
</n-gi>
<n-gi>
<n-text :type="'info'">{{"今开 "+result["今日开盘价"]}}</n-text>
<n-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-gi>
<n-gi>
<n-text :type="'info'">{{"最低 "+result["今日最低价"]+" "+result.lowRate }}%</n-text>
</n-gi>
<n-gi>
<n-text :type="'info'">{{"昨收 "+result["昨日收盘价"]}}</n-text>
</n-gi>
<n-gi>
<n-text :type="'info'">{{"今开 "+result["今日开盘价"]}}</n-text>
</n-gi>
</n-grid>
<template #header-extra>
<n-tag size="small" 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-tag size="small" v-if="result.costPrice>0" :type="result.profitType">{{"成本:"+result.costPrice+" "+result.profit+"%"+" ( "+result.profitAmount+" ¥ )"}}</n-tag>
<n-flex justify="center">
<n-tag size="small" v-if="result.volume>0" :type="result.profitType">{{result.volume+""}}</n-tag>
<n-tag size="small" v-if="result.costPrice>0" :type="result.profitType">{{"成本:"+result.costPrice+"*"+result.costVolume+" "+result.profit+"%"+" ( "+result.profitAmount+" ¥ )"}}</n-tag>
</n-flex>
</template>
<template #action>
<n-flex justify="space-between">
<n-button size="tiny" type="info" @click="setStock(result['股票代码'],result['股票名称'])"> 设置 </n-button>
<n-button size="tiny" type="warning" @click="search(result['股票代码'],result['股票名称'])"> 详情 </n-button>
<n-text :type="'info'">{{result["日期"]+" "+result["时间"]}}</n-text>
<n-button size="tiny" type="info" @click="setStock(result['股票代码'],result['股票名称'])"> 成本 </n-button>
<n-button size="tiny" type="success" @click="showFenshi(result['股票代码'],result['股票名称'])"> 分时 </n-button>
<n-button size="tiny" type="error" @click="showK(result['股票代码'],result['股票名称'])"> 日K </n-button>
<n-button size="tiny" type="warning" @click="search(result['股票代码'],result['股票名称'])"> 详情 </n-button>
</n-flex>
</template>
</n-card >
</n-gi>
</n-grid>
<n-auto-complete v-model:value="data.name" type="text"
:input-props="{
autocomplete: 'disabled',
}"
:options="options"
placeholder="股票名称或者代码"
clearable class="input" @input="getStockList" :on-select="onSelect"/>
<n-button type="info" @click="AddStock"> 添加 </n-button>
<div style="position: fixed;bottom: 18px;right:0;z-index: 10;width: 350px">
<!-- <n-card :bordered="false">-->
<n-input-group>
<!-- <n-button type="error" @click="addBTN=!addBTN" > <n-icon :component="Search"/>&nbsp;<n-text v-if="addBTN">隐藏</n-text></n-button>-->
<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" v-if="addBTN">
<n-icon :component="Add"/> &nbsp;关注该股票
</n-button>
</n-input-group>
<!-- </n-card>-->
</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: '请输入数量'} }" label-placement="left" label-width="80px">
<n-form-item label="成本(元)" path="costPrice">
<n-input-number v-model:value="formModel.costPrice" min="0" placeholder="请输入股票成本" />
<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>
<n-form-item label="数量(股)" path="volume">
<n-input-number v-model:value="formModel.volume" min="0" placeholder="请输入股票数量" />
<n-form-item label="股票数量" path="volume">
<n-input-number v-model:value="formModel.volume" min="0" step="100" placeholder="请输入股票数量" >
<template #suffix>
</template>
</n-input-number>
</n-form-item>
<n-form-item label="涨跌提醒" path="alarm">
<n-input-number v-model:value="formModel.alarm" min="0" placeholder="请输入涨跌报警值(%)" >
<template #suffix>
%
</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)">保存</n-button>
<n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume,formModel.alarm,formModel)">保存</n-button>
</template>
</n-modal>
<n-modal v-model:show="modalShow2" :title="data.name" style="width: 600px" :preset="'card'">
<n-image :src="data.fenshiURL" />
</n-modal>
<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,12 +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 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,10 +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 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

@@ -8,9 +8,12 @@ export namespace data {
Price: number;
PriceChange: number;
ChangePercent: number;
AlarmChangePercent: number;
AlarmPrice: number;
// Go type: time
Time: any;
Sort: number;
IsDel: number;
static createFrom(source: any = {}) {
return new FollowedStock(source);
@@ -25,8 +28,76 @@ export namespace data {
this.Price = source["Price"];
this.PriceChange = source["PriceChange"];
this.ChangePercent = source["ChangePercent"];
this.AlarmChangePercent = source["AlarmChangePercent"];
this.AlarmPrice = source["AlarmPrice"];
this.Time = this.convertValues(source["Time"], null);
this.Sort = source["Sort"];
this.IsDel = source["IsDel"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class Settings {
ID: number;
// Go type: time
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 {
@@ -132,13 +203,14 @@ export namespace data {
"时间": string;
"股票代码": string;
"股票名称": string;
"上次当前价格": number;
"当前价格": string;
"成交的股票数": string;
"成交金额": string;
"今日开盘价": string;
"昨日收盘价": string;
"今日最低价": string;
"今日最高价": string;
"今日最低价": string;
"竞买价": string;
"竞卖价": string;
"买一报价": string;
@@ -161,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);
@@ -176,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["买一报价"];
@@ -205,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 {

34
go.mod
View File

@@ -1,39 +1,63 @@
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
gorm.io/plugin/soft_delete v1.2.1
)
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
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
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
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -45,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

114
go.sum
View File

@@ -1,5 +1,19 @@
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=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -7,6 +21,20 @@ github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvY
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
@@ -17,8 +45,19 @@ github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CA
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
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=
@@ -27,8 +66,12 @@ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4P
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
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=
@@ -44,6 +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=
@@ -53,6 +102,15 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -67,7 +125,9 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -93,25 +153,40 @@ 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=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -122,28 +197,45 @@ golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
@@ -153,11 +245,17 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU=
gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

104
main.go
View File

@@ -6,13 +6,18 @@ import (
"github.com/duke-git/lancet/v2/convertor"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"log"
"os"
goruntime "runtime"
"time"
)
//go:embed frontend/dist
@@ -21,6 +26,9 @@ var assets embed.FS
//go:embed build/appicon.png
var icon []byte
//go:embed build/app.ico
var icon2 []byte
//go:embed build/stock_basic.json
var stocksBin []byte
@@ -31,40 +39,76 @@ func main() {
db.Dao.AutoMigrate(&data.StockInfo{})
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 {
initStockData()
go initStockData()
}
data.NewStockDataApi().GetStockBaseInfo()
updateBasicInfo()
// Create an instance of the app structure
app := NewApp()
AppMenu := menu.NewMenu()
FileMenu := AppMenu.AddSubmenu("设置")
FileMenu.AddText("显示搜索框", keys.CmdOrCtrl("s"), func(callbackData *menu.CallbackData) {
runtime.EventsEmit(app.ctx, "showSearch", 1)
})
FileMenu.AddText("隐藏搜索框", keys.CmdOrCtrl("d"), func(callbackData *menu.CallbackData) {
runtime.EventsEmit(app.ctx, "showSearch", 0)
})
FileMenu.AddText("刷新数据", keys.CmdOrCtrl("r"), func(callbackData *menu.CallbackData) {
//runtime.EventsEmit(app.ctx, "refresh", "setting-"+time.Now().Format("2006-01-02 15:04:05"))
runtime.EventsEmit(app.ctx, "refreshFollowList", "refresh-"+time.Now().Format("2006-01-02 15:04:05"))
})
FileMenu.AddSeparator()
FileMenu.AddText("窗口全屏", keys.CmdOrCtrl("f"), func(callback *menu.CallbackData) {
runtime.WindowFullscreen(app.ctx)
})
FileMenu.AddText("窗口还原", keys.Key("Esc"), func(callback *menu.CallbackData) {
runtime.WindowUnfullscreen(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: nil,
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,
},
@@ -100,6 +144,16 @@ func main() {
if err != nil {
log.Fatal(err)
}
}
func updateBasicInfo() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
if config.UpdateBasicInfoOnStart {
//更新基本信息
go data.NewStockDataApi().GetStockBaseInfo()
go data.NewStockDataApi().GetIndexBasic()
}
}
func initStockData() {

View File

@@ -1 +0,0 @@
{"request_id":"359a096c-0a5a-4b07-818f-3241738d5e73","code":40203,"data":null,"msg":"抱歉您每小时最多访问该接口1次权限的具体详情访问https://tushare.pro/document/1?doc_id=108。"}

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赋能分析股票"
}
}