Compare commits

...

60 Commits

Author SHA1 Message Date
ArvinLovegood
b1a9a8d4d8 refactor(update):优化更新检查逻辑
- 修改 CheckUpdate 函数签名,添加 flag 参数
- 根据 flag 参数控制是否显示"当前版本无更新"的通知
- 调整前端按钮点击事件,传递参数 1 给 CheckUpdate 函数
- 优化后端更新检查流程,减少不必要的通知推送
2025-07-17 17:39:59 +08:00
ArvinLovegood
b98f829286 refactor(update):优化更新检查逻辑
- 修改 CheckUpdate 函数签名,添加 flag 参数
-根据 flag 参数控制是否显示"当前版本无更新"的通知
- 调整前端按钮点击事件,传递参数 1 给 CheckUpdate 函数
- 优化后端更新检查流程,减少不必要的通知推送
2025-07-17 17:31:40 +08:00
ArvinLovegood
dda160069a refactor(app):更新股票数据接口地址
- 将股票数据接口的 HTTPS 地址替换为 HTTP 地址
- 更新接口服务器 IP 和端口
- 此修改影响 A 股、港股和美股的股票数据获取
2025-07-17 14:40:59 +08:00
ArvinLovegood
f80ea181be feat:更新应用标题添加“AI赋能股票分析
- 在 main.go 文件中更新了应用的标题
- 添加了 AI赋能股票分析 和 星星图标,提升应用吸引力
2025-07-16 18:16:04 +08:00
ArvinLovegood
f5c8f5d0ef refactor(mac):显示windows窗体(显示最大最小化按钮)
- 在 Mac 系统中添加编辑菜单
- 注释掉全屏和还原菜单项
- 移除无边框窗口设置
- 调整搜索框和表格样式
- 优化设置页面布局
2025-07-16 18:01:51 +08:00
ArvinLovegood
23d3566f31 feat(app):添加版本更新提示和自定义通知颜色
- 在版本检查无更新时发送通知
- 根据通知来源调整颜色:go-stock 为橙色,其他为蓝色
2025-07-16 12:57:13 +08:00
ArvinLovegood
052104b43a fix(app):修复初次安装软件时股票基础信息没有立即初始化的问题
- 将 CheckStockBaseInfo 方法的调用移到 CheckUpdate 方法之前
- 修改 cron定时任务,只在特定日期执行版本检查和股票基础信息检查
2025-07-16 12:31:40 +08:00
ArvinLovegood
93e8fb27b5 fix(app):修复初次安装软件时股票基础信息没有立即初始化的问题
- 将 CheckStockBaseInfo 方法的调用移到 CheckUpdate 方法之前
- 修改 cron定时任务,只在特定日期执行版本检查和股票基础信息检查
2025-07-16 12:16:15 +08:00
ArvinLovegood
25623d90d7 docs:隐藏 QQ 交流群 2 的链接
- 注释掉了 README.md 中 QQ 交流群 2 的链接
-保留了 QQ 交流群的链接
2025-07-16 09:27:34 +08:00
ArvinLovegood
8db94da233 feat(stock):更新股票基础信息并优化相关功能
- 添加 CheckStockBaseInfo 方法,用于更新股票基础信息
- 修改 domReady 方法,移除初始化股票数据的逻辑
- 更新 StockBasic、StockInfoHK 和 StockInfoUS 模型,添加行业代码和名称字段
- 修改 getDCStockInfo 方法,支持获取更详细的股票信息
- 添加 DCToTsCode 函数,用于将东财代码转换为 TS 代码
- 优化行业报告信息获取功能
2025-07-15 18:49:37 +08:00
ArvinLovegood
60e7d87918 docs(README): 更新 QQ 交流群描述
- 修改了 QQ交流群的描述,从"已满会定期清理,随缘入群"改为"定期清理,随缘入群"
- 此修改反映了群聊状态的更新,使得描述更加准确
2025-07-15 09:44:22 +08:00
ArvinLovegood
615b4d231a refactor(updater):优化软件更新提示内容和样式
- 修改更新提示内容,仅显示 commit message
- 调整通知窗口样式,增加文本对齐和颜色设置
- 更新 README 中的下载链接描述,区分 MACOS 绿色版和安装版
2025-07-14 14:04:42 +08:00
ArvinLovegood
490a3c0847 feat(app):增加恒生科技指数并优化版本更新提示信息
- 在市场组件中添加恒生科技指数选项
- 更新版本时增加提交信息显示
- 优化新版本下载失败提示信息
2025-07-14 11:34:01 +08:00
ArvinLovegood
38f83674ef feat(data):添加PPI和PMI
- 新增 GetPPI 和 GetPMI 函数,用于获取工业品出厂价格指数和采购经理人指数数据
- 添加相关测试用例,验证 PPI 和 PMI接口的功能
- 更新模型结构,支持 PPI 和 PMI 数据
- 在 OpenAI API 中调用新增的 PPI 和 PMI 接口,丰富市场数据信息
2025-07-12 13:31:38 +08:00
ArvinLovegood
d26c4bc986 feat(data):AI市场资讯总结添加国内宏观经济数据(GDP和CPI,后期陆续会加其他数据)
- 在 openai_api.go 中添加 GDP 和 CPI 数据的获取和格式化输出
- 在 market_news_api_test.go 中更新相关测试函数
- 在 struct_to_markdown.go 中新增 MarkdownTableWithTitle 函数用于添加标题
2025-07-11 18:58:31 +08:00
ArvinLovegood
7e919376b5 feat(data):添加GDP和CPI数据接口
- 实现了 GetGDP 和 GetCPI 方法,获取国内生产总值和居民消费价格指数数据
- 新增 GDP 和 CPI 数据模型
- 更新相关测试用例
2025-07-11 18:39:24 +08:00
ArvinLovegood
1d9ef724e6 feat(frontend):重命名"指标行情"标签为"重大指数"
- 重命名"指标行情"标签为"重大指数"
- 新增多个重大指数的行情图表,包括:
  - 科创芯片(sh000685)
  - 证券龙头(sz399437)
  - 高端装备(sz399437)  - 中证银行(sz399986)
  - 上证医药(sh000037)
- 统一设置为暗黑主题
2025-07-11 18:05:45 +08:00
ArvinLovegood
8e982d4430 refactor(main):注释掉隐藏到托盘区的功能
-移除了对 runtime 包的导入
- 注释掉了相关代码块
2025-07-11 17:53:55 +08:00
ArvinLovegood
a67559831a style(frontend):优化K线图和市场组件的拖拽体验
- 在 KLineChart 组件中添加 --wails-draggable:no-drag 样式,禁止拖拽
- 在 Market 组件中调整拖拽样式应用位置,提高用户体验
- 优化 Market 组件的模板结构,移除冗余样式
2025-07-11 17:51:06 +08:00
ArvinLovegood
9718d3311d feat:修改隐藏窗口快捷键为Ctrl+Z
- 将前端 App.vue 文件中的隐藏窗口快捷键从 Ctrl+H 修改为 Ctrl+Z
- 在后端 main.go 文件中添加了隐藏窗口的功能,快捷键也为 Ctrl+Z
- 删除了 main.go 文件中注释掉的隐藏和显示窗口的代码
2025-07-11 16:42:09 +08:00
SparkMemory
789e7427ce Merge pull request #92 from GiCo001/dev-darwin
feat(app): 调整darwin版本的窗口,显示toolbar
2025-07-11 16:18:00 +08:00
Gico001
801aa14c7a feat(app): 调整darwin版本的窗口,显示toolbar 2025-07-11 11:07:46 +08:00
ArvinLovegood
f5c621fbcc refactor(frontend): 优化 Windows 平台下窗口打开方式
- 在 stock.vue 中添加了对 Windows 平台下窗口打开方式的特殊处理
- 指定窗口大小和位置,隐藏菜单栏和工具栏,以实现更佳的用户体验
2025-07-10 17:31:42 +08:00
SparkMemory
119f0f8aa7 Merge pull request #90 from GiCo001/dev-darwin
feat(app): 兼容darwin版本浏览跳转,保存图片文件等功能
2025-07-10 16:31:17 +08:00
ArvinLovegood
fe814974fd feat(util): 添加结构体到 Markdown 表格的转换功能
- 实现了 MarkdownTable 函数,可以将结构体或结构体切片转换为 Markdown 表格格式
- 添加了相关辅助函数,如 markdownSingleStruct、markdownStructSlice、shouldSkip 等
- 示例结构体 User 和 Address 用于演示功能
- 新增 struct_to_markdown_test.go 文件进行测试验证
2025-07-10 16:30:25 +08:00
ArvinLovegood
dd3c231637 feat(data):添加获取国内生产总值(GDP)功能
- 实现了从东财数据中心获取GDP数据的功能
- 新增GDP数据结构用于解析获取的数据
- 添加了获取GDP数据的测试用例
2025-07-10 16:29:40 +08:00
ArvinLovegood
e05ff94aba fix(main):修复不能粘贴的大BUG
- 注释掉了显示搜索框、隐藏搜索框和刷新数据的菜单项
- 注释掉了隐藏到托盘区和显示窗口的菜单项(仅限 Windows)
- 添加了编辑菜单
2025-07-10 16:18:44 +08:00
Gico001
bbd4bb5b48 feat(app): 兼容darwin版本浏览跳转,保存图片文件等功能 2025-07-10 14:49:10 +08:00
ArvinLovegood
58f3009902 feat(frontend):添加微信公众号二维码并更新相关页面
- 在 about.vue 中添加微信公众号二维码图片
- 在 AppInfo 结构中添加 Wxgzh 字段用于存储微信公众号二维码链接
- 在 main.go 中嵌入微信公众号二维码图片
- 在 models 和 TypeScript 中添加相应字段支持微信公众号二维码
2025-07-10 10:01:40 +08:00
ArvinLovegood
c6b841fb8f feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 18:13:00 +08:00
ArvinLovegood
2b28390414 feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 18:11:53 +08:00
ArvinLovegood
7887dfed5e feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 18:06:46 +08:00
ArvinLovegood
a4c98933a4 feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 17:54:32 +08:00
ArvinLovegood
ad63ffff7f feat(sponsor):添加新的赞助计划并实现赞助码验证功能
- 在 about.vue 和 README.md 中添加新的 VIP2 赞助计划
- 在 App.d.ts 和 App.js 中添加 CheckSponsorCode函数
- 在 app.go 中实现 CheckSponsorCode 方法,用于验证赞助码
- 在 settings.vue 中集成赞助码验证功能,更新配置时进行验证
- 优化赞助码输入界面,添加验证按钮
2025-07-09 17:52:58 +08:00
ArvinLovegood
1ccc2f8b1f feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 15:38:52 +08:00
ArvinLovegood
dc5483aa07 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 14:54:26 +08:00
ArvinLovegood
8c82ba4a38 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 14:18:30 +08:00
ArvinLovegood
fd905ff278 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 14:16:31 +08:00
ArvinLovegood
6ec0f5fbe0 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 12:31:53 +08:00
ArvinLovegood
32706fb4dc feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 12:30:13 +08:00
ArvinLovegood
2cb661734f feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 12:27:26 +08:00
ArvinLovegood
4fab910340 feat(frontend):添加支持开源赞助计划
- 在关于页面中增加支持开源赞助计划的表格
- 列出不同赞助等级及其对应的权益说明
- 旨在鼓励用户支持项目发展,提供不同级别的赞助选项
2025-07-09 12:27:08 +08:00
ArvinLovegood
84e4ba8474 feat(update):支持macOS系统更新
- 修改了更新检查逻辑,排除 macOS 系统
- 为 macOS 系统添加了专门的下载链接
- 优化了版本更新提示信息的显示
2025-07-09 11:19:33 +08:00
ArvinLovegood
76a44fae32 feat(update):支持macOS系统更新
- 修改了更新检查逻辑,排除 macOS 系统
- 为 macOS 系统添加了专门的下载链接
- 优化了版本更新提示信息的显示
2025-07-09 10:13:25 +08:00
ArvinLovegood
7ea974f1a6 style(market):为指标行情标签添加不可拖动样式
- 在指标行情标签上添加 style属性,设置 --wails-dragable 为 no-drag
- 这个修改可以防止用户在该标签页中进行不必要的拖动操作,提升用户体验
2025-07-09 09:52:19 +08:00
ArvinLovegood
7ea160b6b5 feat(update):优化软件更新逻辑
- 增加对操作系统类型的判断,非 Windows 系统不执行更新
- 优化更新版本信息的传递方式
-重构代码,提高可读性和可维护性
2025-07-09 09:03:11 +08:00
ArvinLovegood
c2f260c613 feat(update):优化软件更新逻辑
- 增加对操作系统类型的判断,非 Windows 系统不执行更新
- 优化更新版本信息的传递方式
-重构代码,提高可读性和可维护性
2025-07-08 21:08:49 +08:00
ArvinLovegood
2d224ccfc4 feat(update):优化软件更新逻辑
- 增加对操作系统类型的判断,非 Windows 系统不执行更新
- 优化更新版本信息的传递方式
-重构代码,提高可读性和可维护性
2025-07-08 18:53:36 +08:00
ArvinLovegood
a66f2156f1 feat(update):实现软件自动更新功能
- 新增自动检查和下载最新版本的功能
- 使用 go-update 库进行软件更新
- 增加新版本推送通知和更新结果通知
- 优化错误处理和日志记录
2025-07-08 18:45:49 +08:00
ArvinLovegood
e90727773f refactor(frontend): 调整股市通组件内容
-将百度股市通替换为选股通
- 注释掉百度股市通和摸鱼选项
- 添加 naive-ui 组件导入
2025-07-08 17:49:52 +08:00
SparkMemory
89dcb713be Potential fix for code scanning alert no. 4: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-08 12:00:46 +08:00
SparkMemory
6f4b21207d Potential fix for code scanning alert no. 5: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-08 11:57:29 +08:00
SparkMemory
f51e3d863a Potential fix for code scanning alert no. 6: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-08 11:51:26 +08:00
ArvinLovegood
c180c2a5f8 feat(stock):优化股票迷你图刷新逻辑
- 在 Stock 组件中添加 lastPrice 属性,传递当前价格给 StockSparkLine 组件
- 在 StockSparkLine 组件中接收 lastPrice 属性,并使用它来更新 K 线图数据
- 优化 StockSparkLine 组件的渲染逻辑,使用 onMounted 和 watchEffect
2025-07-08 10:58:01 +08:00
ArvinLovegood
3ba18e8ef2 feat(stock):优化股票迷你图刷新逻辑
- 在 Stock 组件中添加 lastPrice 属性,传递当前价格给 StockSparkLine 组件
- 在 StockSparkLine 组件中接收 lastPrice 属性,并使用它来更新 K 线图数据
- 优化 StockSparkLine 组件的渲染逻辑,使用 onMounted 和 watchEffect
2025-07-08 10:50:04 +08:00
ArvinLovegood
f0314187e5 fix(stock):修正开盘价数据源并优化迷你分时图渲染逻辑
- 将 stock.vue 中的开盘价数据源从"今日开盘价"改为"昨日收盘价"
- 在 stockSparkLine.vue 中修改图表初始化方式,使用 document.getElementById 获取图表容器
-为 stockSparkLine.vue 中的图表容器添加唯一的 id 属性,以确保正确渲染多个图表
2025-07-07 17:57:17 +08:00
ArvinLovegood
6440885688 docs(README): 更新更新日志并添加新功能说明
- 新增卡片添加迷你分时图功能
- 新增MacOs支持- 更新现有功能说明
2025-07-07 17:30:37 +08:00
ArvinLovegood
2dd4f072b2 feat(frontend):添加股票分钟线迷你图表并优化界面
- 新增 StockSparkLine 组件,用于显示股票分钟迷你线图表
- 在股票页面中集成 StockSparkLine 组件
- 为 about、market 和 settings 页面的主体元素添加可拖拽样式
- 优化股票页面布局,调整网格列数和对齐方式
2025-07-07 17:17:49 +08:00
ArvinLovegood
b5843bcdb8 refactor(frontend):重构前端路由并优化设置页面布局
- 修改路由配置,使用 createWebHashHistory 替代 createWebHistory- 重命名部分组件以提高代码一致性
- 优化设置页面布局,使用卡片组件分类显示不同设置项
- 调整提示词模板展示方式,增加警告提示
2025-07-07 10:50:08 +08:00
ArvinLovegood
c65d0b79f4 ci: 更新 GitHub Actions 工作流
- 添加对 '*-dev' 标签的匹配,以支持开发版本的构建
-移除 Windows ARM64 构建配置,简化构建流程
2025-07-06 20:58:24 +08:00
36 changed files with 2159 additions and 326 deletions

View File

@@ -5,11 +5,13 @@ on:
tags:
# Match any new tag
- '*-release'
- '*-dev'
env:
# Necessary for most environments as build failure can occur due to OOM issues
NODE_OPTIONS: "--max-old-space-size=4096"
OFFICIAL_STATEMENT: ${{ vars.OFFICIAL_STATEMENT }}
BUILD_KEY: ${{ vars.BUILD_KEY }}
jobs:
build:
@@ -21,10 +23,6 @@ jobs:
- name: 'go-stock-windows-amd64.exe'
platform: 'windows/amd64'
os: 'windows-latest'
- name: 'go-stock-windows-arm64.exe'
platform: 'windows/arm64'
os: 'windows-latest'
arch: arm64
# - name: 'go-stock-linux-amd64'
# platform: 'linux/amd64'
# os: 'ubuntu-latest'
@@ -56,4 +54,5 @@ jobs:
build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
build-statement: ${{ env.OFFICIAL_STATEMENT }}
build-key: ${{ env.BUILD_KEY }}
node-version: '20.x'

View File

@@ -10,8 +10,9 @@
![扫码_搜索联合传播样式-白色版.png](build/screenshot/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png)
### 📈 交流群
- QQ交流群2[点击链接加入群聊【go-stock交流群2】892666282](https://qm.qq.com/q/5mYiy6Yxh0)
- QQ交流群[点击链接加入群聊【go-stock交流群】491605333(已满会定期清理,随缘入群)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
[//]: # (- QQ交流群2[点击链接加入群聊【go-stock交流群2】:892666282]&#40;https://qm.qq.com/q/5mYiy6Yxh0&#41;)
- QQ交流群[点击链接加入群聊【go-stock交流群】491605333(定期清理,随缘入群)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
### ✨ 简介
- 本项目基于Wails和NaiveUI开发结合AI大模型构建的股票分析工具。
@@ -23,6 +24,8 @@
### 📦 立即体验
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
- MACOS绿色版[go-stock-darwin-universal](https://github.com/ArvinLovegood/go-stock/releases)
- MACOS安装版[go-stock-darwin-universal.pkg](https://github.com/ArvinLovegood/go-stock/releases)
### 💬 支持大模型/平台
@@ -44,6 +47,14 @@
- 欢迎大家提出宝贵的建议欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
### 支持开源💕计划
| 赞助计划 | 赞助等级 | 权益说明 |
|:--------------------------------|----------------|:-------------------------------------------------------|
| 每月 0 RMB | vip0 | 🌟 全部功能,软件自动更新(从GitHub下载),自行解决github平台网络问题。 |
| 每月赞助 18.8 RMB<br>每年赞助 120 RMB | vip1 | 💕 全部功能,软件自动更新(从CDN下载),更新快速便捷。AI配置指导提示词参考等 |
| 每月赞助 28.8 RMB<br>每年赞助 240 RMB | vip2 | 💕 💕 vip1全部功能,赠送硅基流动AI分析服务 |
| 每月赞助 X RMB | vipX | 🧩 更多计划视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖) |
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|----------------------------------------------------------------------------------------------------------|
@@ -57,6 +68,9 @@
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
## 👀 更新日志
### 2025.07.08 实现软件自动更新功能
### 2025.07.07 卡片添加迷你分时图
### 2025.07.05 MacOs支持
### 2025.07.01 AI分析集成工具函数AI分析将更加智能
### 2025.06.30 添加指标选股功能
### 2025.06.27 添加财经日历和重大事件时间轴功能

383
app.go
View File

@@ -1,14 +1,20 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
"github.com/inconshreveable/go-update"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"os"
"path/filepath"
"strings"
"time"
@@ -25,11 +31,12 @@ import (
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
cron *cron.Cron
cronEntrys map[string]cron.EntryID
AiTools []data.Tool
ctx context.Context
cache *freecache.Cache
cron *cron.Cron
cronEntrys map[string]cron.EntryID
AiTools []data.Tool
SponsorInfo map[string]any
}
// NewApp creates a new App application struct
@@ -100,7 +107,64 @@ func AddTools(tools []data.Tool) []data.Tool {
return tools
}
func (a *App) CheckUpdate() {
func (a *App) GetSponsorInfo() map[string]any {
return a.SponsorInfo
}
func (a *App) CheckSponsorCode(sponsorCode string) map[string]any {
sponsorCode = strutil.Trim(sponsorCode)
if sponsorCode != "" {
encrypted, err := hex.DecodeString(sponsorCode)
if err != nil {
return map[string]any{
"code": 0,
"msg": "赞助码格式错误,请输入正确的赞助码!",
}
}
key, err := hex.DecodeString(BuildKey)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return map[string]any{
"code": 0,
"msg": "版本错误,不支持赞助码!",
}
}
decrypt := cryptor.AesEcbDecrypt(encrypted, key)
if decrypt == nil || len(decrypt) == 0 {
return map[string]any{
"code": 0,
"msg": "赞助码错误,请输入正确的赞助码!",
}
}
return map[string]any{
"code": 1,
"msg": "赞助码校验成功,感谢您的支持!",
}
} else {
return map[string]any{"code": 0, "message": "赞助码不能为空,请输入正确的赞助码!"}
}
}
func (a *App) CheckUpdate(flag int) {
sponsorCode := strutil.Trim(a.GetConfig().SponsorCode)
if sponsorCode != "" {
encrypted, err := hex.DecodeString(sponsorCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
key, err := hex.DecodeString(BuildKey)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
decrypt := string(cryptor.AesEcbDecrypt(encrypted, key))
err = json.Unmarshal([]byte(decrypt), &a.SponsorInfo)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
}
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
@@ -111,6 +175,7 @@ func (a *App) CheckUpdate() {
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
tag := &models.Tag{}
_, err = resty.New().R().
SetResult(tag).
@@ -118,6 +183,7 @@ func (a *App) CheckUpdate() {
if err == nil {
releaseVersion.Tag = *tag
}
commit := &models.Commit{}
_, err = resty.New().R().
SetResult(commit).
@@ -126,28 +192,143 @@ func (a *App) CheckUpdate() {
releaseVersion.Commit = *commit
}
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
if !(IsWindows() || IsMacOS()) {
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
return
}
downloadUrl := fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
if IsMacOS() {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
}
sponsorCode = strutil.Trim(a.GetConfig().SponsorCode)
if sponsorCode != "" {
encrypted, err := hex.DecodeString(sponsorCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
key, err := hex.DecodeString(BuildKey)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
decrypt := string(cryptor.AesEcbDecrypt(encrypted, key))
err = json.Unmarshal([]byte(decrypt), &a.SponsorInfo)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
vipStartTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipStartTime"].(string), time.Local)
vipEndTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipEndTime"].(string), time.Local)
vipAuthTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipAuthTime"].(string), time.Local)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
isVip := false
if time.Now().After(vipAuthTime) && time.Now().After(vipStartTime) && time.Now().Before(vipEndTime) {
isVip = true
}
if IsWindows() {
if isVip {
if a.SponsorInfo["winDownUrl"] == nil {
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
} else {
downloadUrl = a.SponsorInfo["winDownUrl"].(string)
}
} else {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
}
}
if IsMacOS() {
if isVip {
if a.SponsorInfo["macDownUrl"] == nil {
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
} else {
downloadUrl = a.SponsorInfo["macDownUrl"].(string)
}
} else {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
}
}
}
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "发现新版本:" + releaseVersion.TagName,
"isRed": false,
"source": "go-stock",
"content": fmt.Sprintf("%s", commit.Message),
})
resp, err := resty.New().R().Get(downloadUrl)
if err != nil {
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "新版本:" + releaseVersion.TagName,
"isRed": true,
"source": "go-stock",
"content": commit.Message + "\n新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
})
return
}
body := resp.Body()
if len(body) < 1024 {
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "新版本:" + releaseVersion.TagName,
"isRed": true,
"source": "go-stock",
"content": commit.Message + "\n新版本下载失败,请稍后重试或请前往 https://github.com/ArvinLovegood/go-stock/releases 手动下载替换文件。",
})
return
}
err = update.Apply(bytes.NewReader(body), update.Options{})
if err != nil {
logger.SugaredLogger.Error("更新失败: ", err.Error())
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
return
} else {
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "新版本:" + releaseVersion.TagName,
"isRed": true,
"source": "go-stock",
"content": "版本更新完成,下次重启软件生效.",
})
}
} else {
if flag == 1 {
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "当前版本:" + Version,
"isRed": false,
"source": "go-stock",
"content": "当前版本无更新",
})
}
}
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
defer PanicHandler()
defer func() {
go runtime.EventsEmit(ctx, "loadingMsg", "done")
}()
if stocksBin != nil && len(stocksBin) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
go initStockData(a.ctx)
}
if stocksBinHK != nil && len(stocksBinHK) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
go initStockDataHK(a.ctx)
}
if stocksBinUS != nil && len(stocksBinUS) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
go initStockDataUS(a.ctx)
}
//if stocksBin != nil && len(stocksBin) > 0 {
// go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
// go initStockData(a.ctx)
//}
//
//if stocksBinHK != nil && len(stocksBinHK) > 0 {
// go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
// go initStockDataHK(a.ctx)
//}
//
//if stocksBinUS != nil && len(stocksBinUS) > 0 {
// go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
// go initStockDataUS(a.ctx)
//}
updateBasicInfo()
// Add your action here
@@ -252,12 +433,12 @@ func (a *App) domReady(ctx context.Context) {
}
//检查新版本
go func() {
a.CheckUpdate()
a.CheckUpdate(0)
a.CheckStockBaseInfo(a.ctx)
a.cron.AddFunc("30 05 8,12,20 * * *", func() {
logger.SugaredLogger.Errorf("Checking for updates...")
a.CheckUpdate()
a.CheckUpdate(0)
})
}()
//检查谷歌浏览器
@@ -292,7 +473,73 @@ func (a *App) domReady(ctx context.Context) {
logger.SugaredLogger.Infof("domReady-cronEntrys:%+v", a.cronEntrys)
}
func (a *App) CheckStockBaseInfo(ctx context.Context) {
defer PanicHandler()
defer func() {
go runtime.EventsEmit(ctx, "loadingMsg", "done")
}()
stockBasics := &[]data.StockBasic{}
resty.New().R().
SetHeader("user", "go-stock").
SetResult(stockBasics).
Get("http://8.134.249.145:18080/go-stock/stock_basic.json")
for _, stock := range *stockBasics {
stockInfo := &data.StockBasic{
TsCode: stock.TsCode,
Name: stock.Name,
Symbol: stock.Symbol,
BKCode: stock.BKCode,
BKName: stock.BKName,
}
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).First(stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&data.StockBasic{}).Create(stockInfo)
} else {
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Updates(stockInfo)
}
}
stockHKBasics := &[]models.StockInfoHK{}
resty.New().R().
SetHeader("user", "go-stock").
SetResult(stockHKBasics).
Get("http://8.134.249.145:18080/go-stock/stock_base_info_hk.json")
for _, stock := range *stockHKBasics {
stockInfo := &models.StockInfoHK{
Code: stock.Code,
Name: stock.Name,
BKName: stock.BKName,
BKCode: stock.BKCode,
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).First(stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
} else {
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stock.Code).Updates(stockInfo)
}
}
stockUSBasics := &[]models.StockInfoUS{}
resty.New().R().
SetHeader("user", "go-stock").
SetResult(stockUSBasics).
Get("http://8.134.249.145:18080/go-stock/stock_base_info_us.json")
for _, stock := range *stockUSBasics {
stockInfo := &models.StockInfoUS{
Code: stock.Code,
Name: stock.Name,
BKName: stock.BKName,
BKCode: stock.BKCode,
}
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).First(stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
} else {
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stock.Code).Updates(stockInfo)
}
}
}
func (a *App) NewsPush(news *[]models.Telegraph) {
for _, telegraph := range *news {
//if telegraph.IsRed {
@@ -696,6 +943,7 @@ func (a *App) GetVersionInfo() *models.VersionInfo {
Icon: GetImageBase(icon),
Alipay: GetImageBase(alipay),
Wxpay: GetImageBase(wxpay),
Wxgzh: GetImageBase(wxgzh),
Content: VersionCommit,
OfficialStatement: OFFICIAL_STATEMENT,
}
@@ -829,15 +1077,6 @@ func (a *App) ExportConfig() string {
}
return "导出成功:" + file
}
func getScreenResolution() (int, int, error) {
//user32 := syscall.NewLazyDLL("user32.dll")
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
//
//width, _, _ := getSystemMetrics.Call(0)
//height, _, _ := getSystemMetrics.Call(1)
return int(1366), int(768), nil
}
func (a *App) ShareAnalysis(stockCode, stockName string) string {
//http://go-stock.sparkmemory.top:16688/upload
@@ -1033,3 +1272,81 @@ func (a *App) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]a
slice.Reverse(res)
return res
}
// OpenURL
//
// @Description: 跨平台打开默认浏览器
// @receiver a
// @param url
func (a *App) OpenURL(url string) {
runtime.BrowserOpenURL(a.ctx, url)
}
// SaveImage
//
// @Description: 跨平台保存图片
// @receiver a
// @param name
// @param base64Data
// @return error
func (a *App) SaveImage(name, base64Data string) string {
// 打开保存文件对话框
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "保存图片",
DefaultFilename: name + "AI分析.png",
Filters: []runtime.FileFilter{
{
DisplayName: "PNG 图片",
Pattern: "*.png",
},
},
})
if err != nil || filePath == "" {
return "文件路径,无法保存。"
}
// 解码并保存
decodeString, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "文件内容异常,无法保存。"
}
err = os.WriteFile(filepath.Clean(filePath), decodeString, 0777)
if err != nil {
return "保存结果异常,无法保存。"
}
return filePath
}
// SaveWordFile
//
// @Description: // 跨平台保存word
// @receiver a
// @param filename
// @param base64Data
// @return error
func (a *App) SaveWordFile(filename string, base64Data string) string {
// 弹出保存文件对话框
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "保存 Word 文件",
DefaultFilename: filename,
Filters: []runtime.FileFilter{
{DisplayName: "Word 文件", Pattern: "*.docx"},
},
})
if err != nil || filePath == "" {
return "文件路径,无法保存。"
}
// 解码 base64 内容
decodeString, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "文件内容异常,无法保存。"
}
// 保存为文件
err = os.WriteFile(filepath.Clean(filePath), decodeString, 0777)
if err != nil {
return "保存结果异常,无法保存。"
}
return filePath
}

View File

@@ -67,9 +67,31 @@ func (a *App) startup(ctx context.Context) {
log.Fatalf("系统通知失败: %v", err)
}
}()
go setUpScreen(a)
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
}
func setUpScreen(a *App) {
screens, _ := runtime.ScreenGetAll(a.ctx)
if len(screens) == 0 {
return
}
screen := screens[0]
sw, sh := screen.Width, screen.Height
// macOS 菜单栏 + Dock 留出空间
topBarHeight := 22
dockHeight := 56
verticalMargin := topBarHeight + dockHeight
// 设置窗口为屏幕 80% 宽 × 可用高度 90%
w := int(float64(sw) * 0.8)
h := int(float64(sh-verticalMargin) * 0.9)
runtime.WindowSetSize(a.ctx, w, h)
runtime.WindowCenter(a.ctx)
}
// OnSecondInstanceLaunch 处理第二实例启动时的通知
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
err := beeep.Notify("go-stock", "程序已经在运行了", "")
@@ -166,3 +188,17 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
return false // 如果选择了确定,继续关闭应用
}
}
func getFrameless() bool {
return false
}
func getScreenResolution() (int, int, int, int, error) {
//user32 := syscall.NewLazyDLL("user32.dll")
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
//
//width, _, _ := getSystemMetrics.Call(0)
//height, _, _ := getSystemMetrics.Call(1)
return int(1200), int(800), 0, 0, nil
}

View File

@@ -1,7 +1,10 @@
package main
import (
"encoding/json"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"testing"
"time"
)
@@ -23,3 +26,21 @@ func TestIsUSTradingTime(t *testing.T) {
t.Log(IsUSTradingTime(time.Now()))
}
func TestCheckStockBaseInfo(t *testing.T) {
db.Init("./data/stock.db")
NewApp().CheckStockBaseInfo()
}
func TestJson(t *testing.T) {
db.Init("./data/stock.db")
jsonStr := "{\n\t\t\"id\" : 3334,\n\t\t\"created_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"updated_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"deleted_at\" : null,\n\t\t\"code\" : \"PUK.US\",\n\t\t\"name\" : \"英国保诚集团\",\n\t\t\"full_name\" : \"\",\n\t\t\"e_name\" : \"\",\n\t\t\"exchange\" : \"NASDAQ\",\n\t\t\"type\" : \"stock\",\n\t\t\"is_del\" : 0,\n\t\t\"bk_name\" : null,\n\t\t\"bk_code\" : null\n\t}"
v := &models.StockInfoUS{}
json.Unmarshal([]byte(jsonStr), v)
logger.SugaredLogger.Infof("v:%+v", v)
db.Dao.Model(v).Updates(v)
}

View File

@@ -199,3 +199,17 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
return false
}
}
func getFrameless() bool {
return true
}
func getScreenResolution() (int, int, int, int, error) {
//user32 := syscall.NewLazyDLL("user32.dll")
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
//
//width, _, _ := getSystemMetrics.Call(0)
//height, _, _ := getSystemMetrics.Call(1)
return int(1366), int(768), 1456, 768, nil
}

View File

@@ -15,6 +15,7 @@ import (
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"strconv"
"strings"
"time"
@@ -701,3 +702,158 @@ func (m MarketNewsApi) ClsCalendar() []any {
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["data"].([]any)
}
func (m MarketNewsApi) GetGDP() *models.GDPResp {
res := &models.GDPResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CDOMESTICL_PRODUCT_BASE%2CFIRST_PRODUCT_BASE%2CSECOND_PRODUCT_BASE%2CTHIRD_PRODUCT_BASE%2CSUM_SAME%2CFIRST_SAME%2CSECOND_SAME%2CTHIRD_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_GDP&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
return res
}
body := resp.Body()
logger.SugaredLogger.Debugf("GDP:%s", body)
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
return res
}
data, _ := val.Object().Value().Export()
logger.SugaredLogger.Infof("GDP:%v", data)
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
logger.SugaredLogger.Infof("GDP:%+v", res)
return res
}
func (m MarketNewsApi) GetCPI() *models.CPIResp {
res := &models.CPIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CNATIONAL_SAME%2CNATIONAL_BASE%2CNATIONAL_SEQUENTIAL%2CNATIONAL_ACCUMULATE%2CCITY_SAME%2CCITY_BASE%2CCITY_SEQUENTIAL%2CCITY_ACCUMULATE%2CRURAL_SAME%2CRURAL_BASE%2CRURAL_SEQUENTIAL%2CRURAL_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_CPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
return res
}
body := resp.Body()
logger.SugaredLogger.Debugf("GDP:%s", body)
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
return res
}
data, _ := val.Object().Value().Export()
logger.SugaredLogger.Infof("GDP:%v", data)
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
logger.SugaredLogger.Infof("GDP:%+v", res)
return res
}
// GetPPI PPI
func (m MarketNewsApi) GetPPI() *models.PPIResp {
res := &models.PPIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE,TIME,BASE,BASE_SAME,BASE_ACCUMULATE&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PPI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GDP err:%s", err.Error())
return res
}
body := resp.Body()
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
return res
}
data, _ := val.Object().Value().Export()
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
return res
}
func (m MarketNewsApi) GetPMI() *models.PMIResp {
res := &models.PMIResp{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?callback=data&columns=REPORT_DATE%2CTIME%2CMAKE_INDEX%2CMAKE_SAME%2CNMAKE_INDEX%2CNMAKE_SAME&pageNumber=1&pageSize=20&sortColumns=REPORT_DATE&sortTypes=-1&source=WEB&client=WEB&reportName=RPT_ECONOMY_PMI&p=1&pageNo=1&pageNum=1&_=" + strconv.FormatInt(time.Now().Unix(), 10)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Origin", "https://datacenter.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/cjsj/gdp.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
return res
}
body := resp.Body()
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
return res
}
data, _ := val.Object().Value().Export()
marshal, err := json.Marshal(data)
if err != nil {
return res
}
json.Unmarshal(marshal, &res)
return res
}
func (m MarketNewsApi) GetIndustryReportInfo(infoCode string) {
url := "https://data.eastmoney.com/report/zw_industry.jshtml?infocode=" + infoCode
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "data.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/industry.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("GetIndustryReportInfo err:%s", err.Error())
return
}
body := resp.Body()
//logger.SugaredLogger.Debugf("GetIndustryReportInfo:%s", body)
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
title, _ := doc.Find("div.c-title").Html()
content, _ := doc.Find("div.ctx-content").Html()
//logger.SugaredLogger.Infof("GetIndustryReportInfo:\n%s\n%s", title, content)
markdown, err := util.HTMLToMarkdown(title + content)
if err != nil {
return
}
logger.SugaredLogger.Infof("GetIndustryReportInfo markdown:\n%s", markdown)
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/tidwall/gjson"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/util"
"strings"
"testing"
)
@@ -175,3 +176,31 @@ func TestClsCalendar(t *testing.T) {
}
logger.SugaredLogger.Debugf("md:\n %s", md.String())
}
func TestGetGDP(t *testing.T) {
res := NewMarketNewsApi().GetGDP()
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
logger.SugaredLogger.Debugf(md)
}
func TestGetCPI(t *testing.T) {
res := NewMarketNewsApi().GetCPI()
md := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res.CPIResult.Data)
logger.SugaredLogger.Debugf(md)
}
// PPI
func TestGetPPI(t *testing.T) {
res := NewMarketNewsApi().GetPPI()
md := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res.PPIResult.Data)
logger.SugaredLogger.Debugf(md)
}
// PMI
func TestGetPMI(t *testing.T) {
res := NewMarketNewsApi().GetPMI()
md := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res.PMIResult.Data)
logger.SugaredLogger.Debugf(md)
}
func TestGetIndustryReportInfo(t *testing.T) {
NewMarketNewsApi().GetIndustryReportInfo("AP202507151709216483")
}

View File

@@ -17,6 +17,7 @@ import (
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"strings"
"sync"
"time"
@@ -41,6 +42,11 @@ type OpenAi struct {
BrowserPath string `json:"browser_path"`
}
func (o OpenAi) String() string {
return fmt.Sprintf("OpenAi{BaseUrl: %s, Model: %s, MaxTokens: %d, Temperature: %.2f, Prompt: %s, TimeOut: %d, QuestionTemplate: %s, CrawlTimeOut: %d, KDays: %d, BrowserPath: %s, ApiKey: [MASKED]}",
o.BaseUrl, o.Model, o.MaxTokens, o.Temperature, o.Prompt, o.TimeOut, o.QuestionTemplate, o.CrawlTimeOut, o.KDays, o.BrowserPath)
}
func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
config := GetConfig()
if config.OpenAiEnable {
@@ -140,8 +146,8 @@ func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromp
go func() {
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic: %s", err)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config: %s", o.String())
}
}()
defer close(ch)
@@ -173,7 +179,34 @@ func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromp
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
})
wg := &sync.WaitGroup{}
wg.Add(2)
wg.Add(3)
go func() {
defer wg.Done()
var market strings.Builder
res := NewMarketNewsApi().GetGDP()
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
market.WriteString(md)
res2 := NewMarketNewsApi().GetCPI()
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
market.WriteString(md2)
res3 := NewMarketNewsApi().GetPPI()
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
market.WriteString(md3)
res4 := NewMarketNewsApi().GetPMI()
md4 := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res4.PMIResult.Data)
market.WriteString(md4)
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "国内宏观经济数据",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "\n# 国内宏观经济数据:\n" + market.String(),
})
}()
go func() {
defer wg.Done()
var market strings.Builder
@@ -264,7 +297,7 @@ func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int)
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%s", o.String())
}
}()
defer close(ch)
@@ -356,7 +389,7 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewChatStream goroutine panic :%s", err)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic config:%v", o)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic config:%s", o.String())
}
}()
defer close(ch)

View File

@@ -35,6 +35,7 @@ type Settings struct {
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
SponsorCode string `json:"sponsorCode"`
}
func (receiver Settings) TableName() string {
@@ -80,6 +81,7 @@ func (s SettingsApi) UpdateConfig() string {
"dark_theme": s.Config.DarkTheme,
"enable_fund": s.Config.EnableFund,
"enable_push_news": s.Config.EnablePushNews,
"sponsor_code": s.Config.SponsorCode,
})
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
@@ -108,6 +110,7 @@ func (s SettingsApi) UpdateConfig() string {
DarkTheme: s.Config.DarkTheme,
EnableFund: s.Config.EnableFund,
EnablePushNews: s.Config.EnablePushNews,
SponsorCode: s.Config.SponsorCode,
})
}
return "保存成功!"

View File

@@ -153,6 +153,8 @@ type StockBasic struct {
IsHs string `json:"is_hs"`
ActName string `json:"act_name"`
ActEntType string `json:"act_ent_type"`
BKName string `json:"bk_name"`
BKCode string `json:"bk_code"`
}
type FollowedStock struct {
@@ -1454,7 +1456,7 @@ func getSinaStockInfo(receiver StockDataApi, page, pageSize int) *[]models.SinaS
func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
fs := ""
fs := "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"
switch market {
case "hk":
fs = "m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2"
@@ -1462,62 +1464,108 @@ func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
fs = "m:105,m:106,m:107"
}
url := "https://push2.eastmoney.com/api/qt/clist/get?cb=jQuery371047843066631541353_1745889398012&fs=%s&fields=f12,f13,f14,f19,f1,f2,f4,f3,f152,f17,f18,f15,f16,f5,f6&fid=f3&pn=%d&pz=%d&po=1&dect=1"
url := "https://push2.eastmoney.com/api/qt/clist/get?np=1&fltt=1&invt=2&cb=data&fs=%s&fields=f12,f13,f14,f1,f2,f4,f3,f152,f5,f6,f7,f15,f18,f16,f17,f10,f8,f9,f23,f100,f265&fid=f3&pn=%d&pz=%d&po=1&dect=1&wbp2u=|0|0|0|web&_=%d"
sprintfUrl := fmt.Sprintf(url, fs, page, pageSize, time.Now().UnixMilli())
logger.SugaredLogger.Infof("url:%s", sprintfUrl)
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "push2.eastmoney.com").
SetHeader("Referer", "https://quote.eastmoney.com/center/gridlist.html").
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(url, fs, page, pageSize))
Get(sprintfUrl)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
body := string(resp.Body())
body = strutil.ReplaceWithMap(body, map[string]string{
"jQuery371047843066631541353_1745889398012(": "",
");": "",
})
js := "var d=" + body
logger.SugaredLogger.Infof("resp:%s", body)
vm := otto.New()
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(d);")
value, err := vm.Get("data")
data := make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &data)
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("vm.Run error:%v", err.Error())
}
value, _ := val.Object().Value().Export()
marshal, err := json.Marshal(value)
data := make(map[string]any)
err = json.Unmarshal(marshal, &data)
if err != nil {
logger.SugaredLogger.Errorf("json:%s", value.String())
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
logger.SugaredLogger.Infof("resp:%s", data)
if data["data"] != nil {
datas := data["data"].(map[string]any)
total := datas["total"].(float64)
diff := datas["diff"].(map[string]any)
diff := datas["diff"].([]any)
logger.SugaredLogger.Infof("total:%d", int(total))
for k, item := range diff {
stock := item.(map[string]any)
logger.SugaredLogger.Infof("k:%s,%s:%s", k, stock["f14"], stock["f12"])
logger.SugaredLogger.Infof("k:%d,%s:%s:%s %s:%s", k, stock["f14"], stock["f12"], DCToTsCode(stock["f12"].(string)), stock["f100"], stock["f265"])
if market == "" {
stockInfo := &StockBasic{
Symbol: stock["f12"].(string),
TsCode: DCToTsCode(stock["f12"].(string)),
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&StockBasic{}).Create(stockInfo)
} else {
stockInfo = &StockBasic{
Symbol: stock["f12"].(string),
TsCode: DCToTsCode(stock["f12"].(string)),
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).Updates(stockInfo)
}
}
if market == "hk" {
stockInfo := &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
} else {
stockInfo = &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
}
}
if market == "us" {
stockInfo := &models.StockInfoUS{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
} else {
stockInfo = &models.StockInfoUS{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
BKName: stock["f100"].(string),
BKCode: stock["f265"].(string),
}
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
}
}
@@ -1526,6 +1574,25 @@ func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
}
}
func DCToTsCode(dcCode string) string {
//北京证券交易所 883、87、88 等) 创新型中小企业(专精特新为主)
//上海证券交易所 660、688 等) 大盘蓝筹、科创板(高新技术)
//深圳证券交易所 0、3000、002、30 等) 中小盘、创业板(成长型创新企业)
switch dcCode[0:1] {
case "8":
return dcCode + ".BJ"
case "9":
return dcCode + ".BJ"
case "6":
return dcCode + ".SH"
case "0":
return dcCode + ".SZ"
case "3":
return dcCode + ".SZ"
}
return ""
}
func (receiver StockDataApi) GetHKStockInfo(pageSize int) {
url := "https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=1&order=desc&var_name=list_data"
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().

View File

@@ -110,7 +110,8 @@ func TestGetHKStockInfo(t *testing.T) {
//NewStockDataApi().GetSinaHKStockInfo()
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
for i := 1; i <= 592; i++ {
//287 224 605
for i := 1; i <= 605; i++ {
NewStockDataApi().getDCStockInfo("us", i, 20)
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
}

View File

@@ -155,6 +155,7 @@ type VersionInfo struct {
Icon string `json:"icon"`
Alipay string `json:"alipay"`
Wxpay string `json:"wxpay"`
Wxgzh string `json:"wxgzh"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
OfficialStatement string `json:"officialStatement"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
@@ -171,6 +172,8 @@ type StockInfoHK struct {
FullName string `json:"fullName"`
EName string `json:"eName"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
BKName string `json:"bk_name"`
BKCode string `json:"bk_code"`
}
func (receiver StockInfoHK) TableName() string {
@@ -186,6 +189,8 @@ type StockInfoUS struct {
Exchange string `json:"exchange"`
Type string `json:"type"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
BKName string `json:"bk_name"`
BKCode string `json:"bk_code"`
}
func (receiver StockInfoUS) TableName() string {
@@ -369,3 +374,94 @@ type HotEvent struct {
StatusCount int `json:"status_count"`
Content string `json:"content"`
}
type GDP struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
DOMESTICLPRODUCTBASE float64 `json:"DOMESTICL_PRODUCT_BASE" md:"国内生产总值(亿元)"`
SUMSAME float64 `json:"SUM_SAME" md:"国内生产总值同比增长(%)"`
FIRSTPRODUCTBASE float64 `json:"FIRST_PRODUCT_BASE" md:"第一产业(亿元)"`
FIRSTSAME int `json:"FIRST_SAME" md:"第一产业同比增长(%)"`
SECONDPRODUCTBASE float64 `json:"SECOND_PRODUCT_BASE" md:"第二产业(亿元)"`
SECONDSAME float64 `json:"SECOND_SAME" md:"第二产业同比增长(%)"`
THIRDPRODUCTBASE float64 `json:"THIRD_PRODUCT_BASE" md:"第三产业(亿元)"`
THIRDSAME float64 `json:"THIRD_SAME" md:"第三产业同比增长(%)"`
}
type CPI struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
NATIONALBASE float64 `json:"NATIONAL_BASE" md:"全国当月"`
NATIONALSAME float64 `json:"NATIONAL_SAME" md:"全国当月同比增长(%)"`
NATIONALSEQUENTIAL float64 `json:"NATIONAL_SEQUENTIAL" md:"全国当月环比增长(%)"`
NATIONALACCUMULATE float64 `json:"NATIONAL_ACCUMULATE" md:"全国当月累计"`
CITYBASE float64 `json:"CITY_BASE" md:"城市当月"`
CITYSAME float64 `json:"CITY_SAME" md:"城市当月同比增长(%)"`
CITYSEQUENTIAL float64 `json:"CITY_SEQUENTIAL" md:"城市当月环比增长(%)"`
CITYACCUMULATE int `json:"CITY_ACCUMULATE" md:"城市当月累计"`
RURALBASE float64 `json:"RURAL_BASE" md:"农村当月"`
RURALSAME float64 `json:"RURAL_SAME" md:"农村当月同比增长(%)"`
RURALSEQUENTIAL int `json:"RURAL_SEQUENTIAL" md:"农村当月环比增长(%)"`
RURALACCUMULATE float64 `json:"RURAL_ACCUMULATE" md:"农村当月累计"`
}
type PPI struct {
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
TIME string `json:"TIME" md:"报告期"`
BASE float64 `json:"BASE" md:"当月"`
BASESAME float64 `json:"BASE_SAME" md:"当月同比增长(%)"`
BASEACCUMULATE float64 `json:"BASE_ACCUMULATE" md:"累计"`
}
type PMI struct {
REPORTDATE string `md:"报告时间" json:"REPORT_DATE"`
TIME string `md:"报告期" json:"TIME"`
MAKEINDEX float64 `md:"制造业指数" json:"MAKE_INDEX"`
MAKESAME float64 `md:"制造业指数同比增长(%)" json:"MAKE_SAME"`
NMAKEINDEX float64 `md:"非制造业" json:"NMAKE_INDEX"`
NMAKESAME float64 `md:"非制造业同比增长(%)" json:"NMAKE_SAME"`
}
type DCResp struct {
Version string `json:"version"`
Success bool `json:"success"`
Message string `json:"message"`
Code int `json:"code"`
}
type GDPResult struct {
Pages int `json:"pages"`
Data []GDP `json:"data"`
Count int `json:"count"`
}
type CPIResult struct {
Pages int `json:"pages"`
Data []CPI `json:"data"`
Count int `json:"count"`
}
type PPIResult struct {
Pages int `json:"pages"`
Data []PPI `json:"data"`
Count int `json:"count"`
}
type PMIResult struct {
Pages int `json:"pages"`
Data []PMI `json:"data"`
Count int `json:"count"`
}
type GDPResp struct {
DCResp
GDPResult GDPResult `json:"result"`
}
type CPIResp struct {
DCResp
CPIResult CPIResult `json:"result"`
}
type PPIResp struct {
DCResp
PPIResult PPIResult `json:"result"`
}
type PMIResp struct {
DCResp
PMIResult PMIResult `json:"result"`
}

View File

@@ -0,0 +1,221 @@
package util
// @Author spark
// @Date 2025/7/15 14:08
// @Desc
//-----------------------------------------------------------------------------------
import (
"bytes"
"fmt"
"golang.org/x/net/html"
"strings"
)
// HTMLNode 表示HTML文档中的一个节点
type HTMLNode struct {
Type html.NodeType
Data string
Attr []html.Attribute
Children []*HTMLNode
}
// HTMLToMarkdown 将HTML转换为Markdown
func HTMLToMarkdown(htmlContent string) (string, error) {
doc, err := html.Parse(strings.NewReader(htmlContent))
if err != nil {
return "", err
}
root := parseHTMLNode(doc)
var buf bytes.Buffer
convertNode(&buf, root, 0)
return buf.String(), nil
}
// parseHTMLNode 递归解析HTML节点
func parseHTMLNode(n *html.Node) *HTMLNode {
node := &HTMLNode{
Type: n.Type,
Data: n.Data,
Attr: n.Attr,
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
node.Children = append(node.Children, parseHTMLNode(c))
}
return node
}
// convertNode 递归转换节点为Markdown
func convertNode(buf *bytes.Buffer, node *HTMLNode, depth int) {
switch node.Type {
case html.ElementNode:
convertElementNode(buf, node, depth)
case html.TextNode:
// 处理文本节点,去除多余的空白
text := strings.TrimSpace(node.Data)
if text != "" {
buf.WriteString(text)
}
}
// 递归处理子节点
for _, child := range node.Children {
convertNode(buf, child, depth+1)
}
// 处理需要在结束标签后添加内容的元素
switch node.Data {
case "p", "h1", "h2", "h3", "h4", "h5", "h6", "li":
buf.WriteString("\n\n")
case "blockquote":
buf.WriteString("\n")
}
}
// convertElementNode 转换元素节点为Markdown
func convertElementNode(buf *bytes.Buffer, node *HTMLNode, depth int) {
switch node.Data {
case "h1":
buf.WriteString("# ")
case "h2":
buf.WriteString("## ")
case "h3":
buf.WriteString("### ")
case "h4":
buf.WriteString("#### ")
case "h5":
buf.WriteString("##### ")
case "h6":
buf.WriteString("###### ")
case "p":
// 段落标签不需要特殊标记,直接处理内容
case "strong", "b":
buf.WriteString("**")
case "em", "i":
buf.WriteString("*")
case "u":
buf.WriteString("<u>")
case "s", "del":
buf.WriteString("~~")
case "a":
//href := getAttrValue(node.Attr, "href")
buf.WriteString("[")
case "img":
src := getAttrValue(node.Attr, "src")
alt := getAttrValue(node.Attr, "alt")
buf.WriteString(fmt.Sprintf("![%s](%s)", alt, src))
case "ul":
// 无序列表不需要特殊标记,子项会处理
case "ol":
// 有序列表不需要特殊标记,子项会处理
case "li":
if isParentListType(node, "ul") {
buf.WriteString("- ")
} else {
// 计算当前列表项的序号
index := 1
if parent := findParentList(node); parent != nil {
for i, sibling := range parent.Children {
if sibling == node {
index = i + 1
break
}
}
}
buf.WriteString(fmt.Sprintf("%d. ", index))
}
case "blockquote":
buf.WriteString("> ")
case "code":
if isParentPre(node) {
// 父节点是pre使用代码块
buf.WriteString("\n```\n")
} else {
// 行内代码
buf.WriteString("`")
}
case "pre":
// 前置代码块由子节点code处理
case "br":
buf.WriteString("\n")
case "hr":
buf.WriteString("\n---\n")
}
// 处理闭合标签
if needsClosingTag(node.Data) {
defer func() {
switch node.Data {
case "strong", "b":
buf.WriteString("**")
case "em", "i":
buf.WriteString("*")
case "u":
buf.WriteString("</u>")
case "s", "del":
buf.WriteString("~~")
case "a":
href := getAttrValue(node.Attr, "href")
buf.WriteString(fmt.Sprintf("](%s)", href))
case "code":
if isParentPre(node) {
buf.WriteString("\n```\n")
} else {
buf.WriteString("`")
}
}
}()
}
}
// getAttrValue 获取属性值
func getAttrValue(attrs []html.Attribute, key string) string {
for _, attr := range attrs {
if attr.Key == key {
return attr.Val
}
}
return ""
}
// isParentListType 检查父节点是否为指定类型的列表
func isParentListType(node *HTMLNode, listType string) bool {
parent := findParentList(node)
return parent != nil && parent.Data == listType
}
// findParentList 查找父列表节点
func findParentList(node *HTMLNode) *HTMLNode {
// 简化实现,实际应该递归查找父节点
if node.Type == html.ElementNode && (node.Data == "ul" || node.Data == "ol") {
return node
}
return nil
}
// isParentPre 检查父节点是否为pre
func isParentPre(node *HTMLNode) bool {
if len(node.Children) == 0 {
return false
}
for _, child := range node.Children {
if child.Type == html.ElementNode && child.Data == "pre" {
return true
}
}
return false
}
// needsClosingTag 判断元素是否需要闭合标签
func needsClosingTag(tag string) bool {
switch tag {
case "img", "br", "hr", "input", "meta", "link":
return false
default:
return true
}
}

View File

@@ -0,0 +1,6 @@
package util
// @Author spark
// @Date 2025/7/15 14:08
// @Desc
//-----------------------------------------------------------------------------------

View File

@@ -0,0 +1,284 @@
package util
import (
"fmt"
"reflect"
"strings"
)
// MarkdownTable 生成结构体或结构体切片的Markdown表格表示
func MarkdownTable(v interface{}) string {
value := reflect.ValueOf(v)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
// 处理单个结构体
if value.Kind() == reflect.Struct {
return markdownSingleStruct(value)
}
// 处理结构体切片/数组
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
if value.Len() == 0 {
return "切片/数组为空"
}
return markdownStructSlice(value)
}
return "输入必须是结构体、结构体指针、结构体切片或数组"
}
func MarkdownTableWithTitle(title string, v interface{}) string {
value := reflect.ValueOf(v)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
// 处理单个结构体
if value.Kind() == reflect.Struct {
return markdownSingleStruct(value)
}
// 处理结构体切片/数组
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
if value.Len() == 0 {
return "\n## " + title + "\n" + "无数据" + "\n"
}
return "\n## " + title + "\n" + markdownStructSlice(value) + "\n"
}
return "\n## " + title + "\n" + "无数据" + "\n"
}
// 处理单个结构体
func markdownSingleStruct(value reflect.Value) string {
t := value.Type()
var b strings.Builder
// 表头
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
b.WriteString(fmt.Sprintf(" %s |", getFieldName(field)))
}
b.WriteString("\n")
// 分隔线
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
b.WriteString(" --- |")
}
b.WriteString("\n")
// 数据行
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
fieldValue := value.Field(i)
b.WriteString(fmt.Sprintf(" %s |", formatValue(fieldValue)))
}
b.WriteString("\n")
return b.String()
}
// 处理结构体切片/数组
func markdownStructSlice(value reflect.Value) string {
if value.Len() == 0 {
return "切片/数组为空"
}
firstElem := value.Index(0)
if firstElem.Kind() == reflect.Ptr {
firstElem = firstElem.Elem()
}
if firstElem.Kind() != reflect.Struct {
return "切片/数组元素必须是结构体或结构体指针"
}
t := firstElem.Type()
var b strings.Builder
// 表头
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
b.WriteString(fmt.Sprintf(" %s |", getFieldName(field)))
}
b.WriteString("\n")
// 分隔线
b.WriteString("|")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if shouldSkip(field) {
continue
}
b.WriteString(" --- |")
}
b.WriteString("\n")
// 多行数据
for i := 0; i < value.Len(); i++ {
elem := value.Index(i)
if elem.Kind() == reflect.Ptr {
elem = elem.Elem()
}
b.WriteString("|")
for j := 0; j < t.NumField(); j++ {
field := t.Field(j)
if shouldSkip(field) {
continue
}
fieldValue := elem.Field(j)
b.WriteString(fmt.Sprintf(" %s |", formatValue(fieldValue)))
}
b.WriteString("\n")
}
return b.String()
}
// 判断是否应该跳过该字段
func shouldSkip(field reflect.StructField) bool {
return field.Tag.Get("md") == "-"
}
// 获取字段的Markdown表头名称
func getFieldName(field reflect.StructField) string {
name := field.Tag.Get("md")
if name == "" || name == "-" {
return field.Name
}
return name
}
// 格式化字段值为字符串
func formatValue(value reflect.Value) string {
if !value.IsValid() {
return "n/a"
}
// 处理指针
if value.Kind() == reflect.Ptr {
if value.IsNil() {
return "nil"
}
return formatValue(value.Elem())
}
// 处理结构体
if value.Kind() == reflect.Struct {
var fields []string
for i := 0; i < value.NumField(); i++ {
field := value.Type().Field(i)
if shouldSkip(field) {
continue
}
fieldValue := value.Field(i)
fields = append(fields, fmt.Sprintf("%s: %s", getFieldName(field), formatValue(fieldValue)))
}
return "{" + strings.Join(fields, ", ") + "}"
}
// 处理切片/数组
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
var items []string
for i := 0; i < value.Len(); i++ {
items = append(items, formatValue(value.Index(i)))
}
return "[" + strings.Join(items, ", ") + "]"
}
// 处理映射
if value.Kind() == reflect.Map {
var items []string
for _, key := range value.MapKeys() {
keyStr := formatValue(key)
valueStr := formatValue(value.MapIndex(key))
items = append(items, fmt.Sprintf("%s: %s", keyStr, valueStr))
}
return "{" + strings.Join(items, ", ") + "}"
}
// 基本类型
return fmt.Sprintf("%v", value.Interface())
}
// 示例结构体
type Address struct {
City string `md:"城市"`
Country string `md:"国家"`
}
type User struct {
Name string `md:"姓名"`
Age int `md:"年龄"`
Email string `md:"邮箱"`
Address Address `md:"地址"`
Phones []string `md:"电话"`
Active bool `md:"活跃状态"`
}
func main() {
// 示例使用:单个结构体
user := User{
Name: "张三",
Age: 30,
Email: "zhangsan@example.com",
Address: Address{
City: "北京",
Country: "中国",
},
Phones: []string{"13800138000", "13900139000"},
Active: true,
}
fmt.Println("单个结构体转换:")
fmt.Println(MarkdownTable(user))
fmt.Println()
// 示例使用:结构体切片
users := []User{
{
Name: "张三",
Age: 30,
Email: "zhangsan@example.com",
Address: Address{
City: "北京",
Country: "中国",
},
Phones: []string{"13800138000"},
Active: true,
},
{
Name: "李四",
Age: 25,
Email: "lisi@example.com",
Address: Address{
City: "上海",
Country: "中国",
},
Phones: []string{"13900139000", "13700137000"},
Active: false,
},
}
fmt.Println("结构体切片转换:")
fmt.Println(MarkdownTable(users))
}

View File

@@ -0,0 +1,54 @@
package util
import (
"fmt"
"testing"
)
func TestMd(t *testing.T) {
// 示例使用:单个结构体
user := User{
Name: "张三",
Age: 30,
Email: "zhangsan@example.com",
Address: Address{
City: "北京",
Country: "中国",
},
Phones: []string{"13800138000", "13900139000"},
Active: true,
}
fmt.Println("单个结构体转换:")
fmt.Println(MarkdownTable(user))
fmt.Println()
// 示例使用:结构体切片
users := []User{
{
Name: "张三",
Age: 30,
Email: "zhangsan@example.com",
Address: Address{
City: "北京",
Country: "中国",
},
Phones: []string{"13800138000"},
Active: true,
},
{
Name: "李四",
Age: 25,
Email: "lisi@example.com",
Address: Address{
City: "上海",
Country: "中国",
},
Phones: []string{"13900139000", "13700137000"},
Active: false,
},
}
fmt.Println("结构体切片转换:")
fmt.Println(MarkdownTable(users))
}

View File

@@ -10,7 +10,7 @@ import {
} from '../wailsjs/runtime'
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
import {RouterLink, useRouter} from 'vue-router'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,NButton,dateZhCN,zhCN} from 'naive-ui'
import {
AlarmOutline,
AnalyticsOutline,
@@ -44,9 +44,9 @@ const enableNews = ref(false)
const contentStyle = ref("")
const enableFund = ref(false)
const enableDarkTheme = ref(null)
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎\n\n未经授权,禁止商业目的!')
const content = ref('未经授权,禁止商业目的!\n\n数据来源于网络,仅供参考;投资有风险,入市需谨慎')
const isFullscreen = ref(false)
const activeKey = ref('')
const activeKey = ref('stock')
const containerRef = ref({})
const realtimeProfit = ref(0)
const telegraph = ref([])
@@ -64,7 +64,10 @@ const menuOptions = ref([
groupId: 0,
},
params: {},
}
},
onClick: () => {
activeKey.value = 'stock'
},
},
{default: () => '股票自选',}
),
@@ -79,6 +82,7 @@ const menuOptions = ref([
href: '#',
type: 'info',
onClick: () => {
activeKey.value = 'stock'
//console.log("push",item)
router.push({
name: 'stock',
@@ -114,6 +118,7 @@ const menuOptions = ref([
params: {}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
@@ -135,6 +140,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
@@ -156,6 +162,7 @@ const menuOptions = ref([
},
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
},
},
@@ -173,14 +180,15 @@ const menuOptions = ref([
to: {
name: 'market',
query: {
name: "指标行情",
name: "重大指数",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '重大指数'})
},
},
{default: () => '指标行情',}
{default: () => '重大指数',}
),
key: 'market3',
icon: renderIcon(AnalyticsOutline),
@@ -198,6 +206,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
},
},
@@ -219,6 +228,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
},
},
@@ -240,6 +250,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
},
},
@@ -261,6 +272,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
},
},
@@ -282,6 +294,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
},
},
@@ -303,6 +316,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
},
},
@@ -324,6 +338,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
},
},
@@ -345,6 +360,7 @@ const menuOptions = ref([
}
},
onClick: () => {
activeKey.value = 'market'
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
},
},
@@ -362,8 +378,13 @@ const menuOptions = ref([
{
to: {
name: 'fund',
params: {},
}
query: {
name: '基金自选',
},
},
onClick: () => {
activeKey.value = 'fund'
},
},
{default: () => '基金自选',}
),
@@ -386,7 +407,12 @@ const menuOptions = ref([
{
to: {
name: 'settings',
params: {}
query: {
name:"设置",
},
onClick: () => {
activeKey.value = 'settings'
},
}
},
{default: () => '设置'}
@@ -401,8 +427,13 @@ const menuOptions = ref([
{
to: {
name: 'about',
params: {}
}
query: {
name:"关于",
}
},
onClick: () => {
activeKey.value = 'about'
},
},
{default: () => '关于'}
),
@@ -422,7 +453,7 @@ const menuOptions = ref([
label: () => h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H',
title: '隐藏到托盘区 Ctrl+Z',
}, {default: () => '隐藏到托盘区'}),
key: 'hide',
icon: renderIcon(ReorderTwoOutline),
@@ -451,6 +482,7 @@ function renderIcon(icon) {
}
function toggleFullscreen(e) {
activeKey.value = 'full'
//console.log(e)
if (isFullscreen.value) {
WindowUnfullscreen()
@@ -606,16 +638,24 @@ onMounted(() => {
//type:"error",
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
title: data.time,
content: () => h(NText,{type:"error"}, { default: () => data.content }),
content: () => h('div',{type:"error",style:{
"text-align":"left",
"font-size":"14px",
"color":"#f67979"
}}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*40,
})
}else{
notification.create({
notification.create({
//type:"info",
//avatar: () => h(NIcon,{component:Notifications}),
title: data.time,
content: () => h(NText,{type:"info"}, { default: () => data.content }),
content: () => h('div',{type:"info",style:{
"text-align":"left",
"font-size":"14px",
"color": data.source==="go-stock"?"#F98C24":"#549EC8"
}}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*30 ,
})

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotTopic} from "../../wailsjs/go/main/App";
import {HotTopic, OpenURL} from "../../wailsjs/go/main/App";
import {Environment} from "../../wailsjs/runtime";
const list = ref([])
const task =ref()
@@ -18,11 +19,20 @@ function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
return window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
);
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
)
break
default:
OpenURL(url)
break
}
})
}
function showPage(htid) {
openCenteredWindow(`https://gubatopic.eastmoney.com/topic_v3.html?htid=${htid}`, 1000, 600)

View File

@@ -378,7 +378,7 @@ function calculateMA(dayCount,values) {
</script>
<template>
<div ref="kLineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
<div ref="kLineChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
</template>
<style scoped>

View File

@@ -1,16 +1,19 @@
<script setup lang="ts">
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {SearchStock,GetHotStrategy} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag,NButton} from 'naive-ui'
import {SearchStock, GetHotStrategy, OpenURL} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag, NButton} from 'naive-ui'
import {Environment} from "../../wailsjs/runtime"
import {RefreshCircleSharp} from "@vicons/ionicons5";
const message = useMessage()
const search = ref('')
const columns = ref([])
const dataList = ref([])
const hotStrategy = ref([])
const traceInfo = ref('')
function Search() {
if(!search.value){
if (!search.value) {
message.warning('请输入选股指标或者要求')
return
}
@@ -18,69 +21,69 @@ function Search() {
const loading = message.loading("正在获取选股数据...", {duration: 0});
SearchStock(search.value).then(res => {
loading.destroy()
// console.log(res)
if(res.code==100){
traceInfo.value=res.data.traceInfo.showText
// message.success(res.msg)
columns.value=res.data.result.columns.filter(item=>!item.hiddenNeed&&(item.title!="市场码"&&item.title!="市场简称")).map(item=>{
if(item.children){
// console.log(res)
if (res.code == 100) {
traceInfo.value = res.data.traceInfo.showText
// message.success(res.msg)
columns.value = res.data.result.columns.filter(item => !item.hiddenNeed && (item.title != "市场码" && item.title != "市场简称")).map(item => {
if (item.children) {
return {
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
key: item.key,
resizable: true,
minWidth:200,
minWidth: 200,
ellipsis: {
tooltip: true
},
children:item.children.filter(item=>!item.hiddenNeed).map(item=>{
children: item.children.filter(item => !item.hiddenNeed).map(item => {
return {
title:item.dateMsg,
key:item.key,
minWidth:100,
title: item.dateMsg,
key: item.key,
minWidth: 100,
resizable: true,
ellipsis: {
tooltip: true
},
sorter: (row1, row2) => {
if(isNumeric(row1[item.key])&&isNumeric(row2[item.key])){
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
return row1[item.key] - row2[item.key];
}else{
} else {
return 'default'
}
},
}
})
}
}else{
} else {
return {
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
key: item.key,
resizable: true,
minWidth:120,
minWidth: 120,
ellipsis: {
tooltip: true
},
sorter: (row1, row2) => {
if(isNumeric(row1[item.key])&&isNumeric(row2[item.key])){
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
return row1[item.key] - row2[item.key];
}else{
} else {
return 'default'
}
},
}
}
})
dataList.value=res.data.result.dataList
}else {
message.error(res.msg)
}
dataList.value = res.data.result.dataList
} else {
message.error(res.msg)
}
}).catch(err => {
message.error(err)
})
}
function isNumeric(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
}
@@ -88,9 +91,9 @@ function isNumeric(value) {
onBeforeMount(() => {
GetHotStrategy().then(res => {
console.log(res)
if(res.code==1){
hotStrategy.value=res.data
search.value=hotStrategy.value[0].question
if (res.code == 1) {
hotStrategy.value = res.data
search.value = hotStrategy.value[0].question
Search()
}
}).catch(err => {
@@ -98,8 +101,9 @@ onBeforeMount(() => {
})
})
function DoSearch(question){
search.value= question
function DoSearch(question) {
search.value = question
Search()
}
@@ -107,60 +111,70 @@ function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
return window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
);
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
)
break
default:
OpenURL(url)
}
})
}
</script>
<template>
<n-grid :cols="24" style="max-height: calc(100vh - 170px)">
<n-gi :span="4" >
<n-list bordered style="text-align: left;" hoverable clickable>
<n-scrollbar style="max-height: calc(100vh - 170px);" >
<n-list-item v-for="item in hotStrategy" :key="item.rank" @click="DoSearch(item.question)">
<n-ellipsis line-clamp="1" :tooltip="true" >
<n-tag size="small" :bordered="false" type="info">#{{item.rank}}</n-tag><n-text type="warning">{{item.question }}</n-text>
<template #tooltip>
<div style="text-align: center;max-width: 180px">
<n-text type="warning">{{item.question }}</n-text>
</div>
</template>
</n-ellipsis>
</n-list-item>
<n-grid :cols="24" style="max-height: calc(100vh - 165px)">
<n-gi :span="4">
<n-list bordered style="text-align: left;" hoverable clickable>
<n-scrollbar style="max-height: calc(100vh - 170px);">
<n-list-item v-for="item in hotStrategy" :key="item.rank" @click="DoSearch(item.question)">
<n-ellipsis line-clamp="1" :tooltip="true">
<n-tag size="small" :bordered="false" type="info">#{{ item.rank }}</n-tag>
<n-text type="warning">{{ item.question }}</n-text>
<template #tooltip>
<div style="text-align: center;max-width: 180px">
<n-text type="warning">{{ item.question }}</n-text>
</div>
</template>
</n-ellipsis>
</n-list-item>
</n-scrollbar>
</n-list>
<!-- <n-virtual-list :items="hotStrategy" :item-size="hotStrategy.length">-->
<!-- <template #default="{ item, index }">-->
<!-- <n-card :title="''" size="small">-->
<!-- <template #header-extra>-->
<!-- {{item.rank}}-->
<!-- </template>-->
<!-- <n-ellipsis expand-trigger="click" line-clamp="3" :tooltip="false" >-->
<!-- <n-text type="warning">{{item.question }}</n-text>-->
<!-- </n-ellipsis>-->
<!-- </n-card>-->
<!-- <n-virtual-list :items="hotStrategy" :item-size="hotStrategy.length">-->
<!-- <template #default="{ item, index }">-->
<!-- <n-card :title="''" size="small">-->
<!-- <template #header-extra>-->
<!-- {{item.rank}}-->
<!-- </template>-->
<!-- <n-ellipsis expand-trigger="click" line-clamp="3" :tooltip="false" >-->
<!-- <n-text type="warning">{{item.question }}</n-text>-->
<!-- </n-ellipsis>-->
<!-- </n-card>-->
<!-- </template>-->
<!-- </n-virtual-list>-->
<!-- </template>-->
<!-- </n-virtual-list>-->
</n-gi>
<n-gi :span="20" >
<n-flex>
<n-gi :span="20">
<n-flex style="--wails-draggable:no-drag">
<n-input-group style="text-align: left">
<n-input :rows="1" clearable v-model:value="search" placeholder="请输入选股指标或者要求" />
<n-input :rows="1" clearable v-model:value="search" placeholder="请输入选股指标或者要求"/>
<n-button type="primary" @click="Search">搜索A股</n-button>
</n-input-group>
</n-flex>
<n-flex justify="start" v-if="traceInfo" style="margin: 5px 0">
<n-flex justify="start" v-if="traceInfo" style="margin: 5px 0;--wails-draggable:no-drag">
<n-ellipsis line-clamp="1" :tooltip="true" >
<n-text type="info" :bordered="false">选股条件</n-text><n-text type="warning" :bordered="true">{{traceInfo}}</n-text>
<n-ellipsis line-clamp="1" :tooltip="true">
<n-text type="info" :bordered="false">选股条件</n-text>
<n-text type="warning" :bordered="true">{{ traceInfo }}</n-text>
<template #tooltip>
<div style="text-align: center;max-width: 580px">
<n-text type="warning">{{traceInfo}}</n-text>
<n-text type="warning">{{ traceInfo }}</n-text>
</div>
</template>
</n-ellipsis>
@@ -169,11 +183,11 @@ function openCenteredWindow(url, width, height) {
</n-flex>
<n-data-table
:striped="true"
:max-height="'calc(100vh - 250px)'"
:max-height="'calc(100vh - 150px)'"
size="medium"
:columns="columns"
:data="dataList"
:pagination="{pageSize: 9}"
:pagination="{pageSize: 10}"
:scroll-x="1800"
:render-cell="(value, rowData, column) => {
@@ -204,7 +218,10 @@ function openCenteredWindow(url, width, height) {
}
}"
/>
<n-text>共找到<n-tag type="info" :bordered="false">{{dataList.length}}</n-tag>只股</n-text>
<div style="margin-top: -25px">共找到
<n-tag type="info" :bordered="false">{{ dataList.length }}</n-tag>
只股
</div>
</n-gi>
</n-grid>

View File

@@ -3,15 +3,19 @@
// preview.css相比style.css少了编辑器那部分样式
import 'md-editor-v3/lib/preview.css';
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import {CheckUpdate, GetVersionInfo,GetSponsorInfo,OpenURL} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn,Environment} from "../../wailsjs/runtime";
import {NAvatar, NButton, useNotification} from "naive-ui";
const updateLog = ref('');
const versionInfo = ref('');
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
const alipay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/alipay.jpg')
const wxpay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/wxpay.jpg')
const wxgzh =ref('https://github.com/ArvinLovegood/go-stock/raw/dev/build/screenshot/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png')
const notify = useNotification()
const vipLevel=ref("");
const vipStartTime=ref("");
const vipEndTime=ref("");
onMounted(() => {
document.title = '关于软件';
@@ -21,7 +25,18 @@ onMounted(() => {
icon.value = res.icon;
alipay.value=res.alipay;
wxpay.value=res.wxpay;
wxgzh.value=res.wxgzh;
GetSponsorInfo().then((res) => {
vipLevel.value = res.vipLevel;
vipStartTime.value = res.vipStartTime;
vipEndTime.value = res.vipEndTime;
})
});
})
onBeforeUnmount(() => {
notify.destroyAll()
@@ -70,7 +85,16 @@ EventsOn("updateVersion",async (msg) => {
type: 'primary',
size: 'small',
onClick: () => {
window.open(msg.html_url)
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(msg.html_url)
break
default :
OpenURL(msg.html_url)
break
}
})
}
}, { default: () => '查看' })
}
@@ -80,21 +104,22 @@ EventsOn("updateVersion",async (msg) => {
</script>
<template>
<n-space vertical size="large">
<n-space vertical size="large" style="--wails-draggable:drag">
<!-- 软件描述 -->
<n-card size="large">
<n-divider title-placement="center">关于软件</n-divider>
<n-space vertical >
<n-image width="100" :src="icon" />
<h1>
<n-badge :value="versionInfo" :offset="[50,10]" type="success">
<n-badge v-if="!vipLevel" :value="versionInfo" :offset="[50,10]" type="success">
<n-gradient-text type="info" :size="50" >go-stock</n-gradient-text>
</n-badge>
<n-badge v-if="vipLevel" :value="versionInfo" :offset="[50,10]" type="success">
<n-gradient-text type="warning" :size="50" >go-stock</n-gradient-text><n-tag :bordered="false" size="small" type="warning">VIP{{vipLevel}}</n-tag>
</n-badge>
</h1>
<n-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
<n-gradient-text type="warning" v-if="vipLevel" >vip到期时间{{vipEndTime}}</n-gradient-text>
<n-button size="tiny" @click="CheckUpdate(1)" type="info" tertiary >检查更新</n-button>
<div style="justify-self: center;text-align: left" >
<p>自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
<p>目前已支持A股港股美股未来计划加入基金ETF等支持</p>
@@ -113,14 +138,39 @@ EventsOn("updateVersion",async (msg) => {
<p>QQ交流群<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333" target="_blank">491605333</a></p>
</div>
</n-space>
<n-divider title-placement="center">支持💕开源</n-divider>
<n-flex justify="center">
<n-table size="small" style="width: 820px">
<n-thead>
<n-tr>
<n-th>赞助计划</n-th>
<n-th>赞助等级</n-th>
<n-th>权益说明</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr>
<n-td>每月 0 RMB</n-td><n-td>vip0</n-td><n-td>🌟 全部功能,软件自动更新(从GitHub下载),自行解决github平台网络问题</n-td>
</n-tr>
<n-tr>
<n-td>赞助 18.8 RMB/<br>赞助 120 RMB/</n-td><n-td>vip1</n-td><n-td>💕 全部功能,软件自动更新(从CDN下载),更新快速便捷AI配置指导提示词参考等</n-td>
</n-tr>
<n-tr>
<n-td>赞助 28.8 RMB/<br>赞助 240 RMB/</n-td><n-td>vip2</n-td><n-td>💕 vip1全部功能,赠送硅基流动AI分析服务💕</n-td>
</n-tr>
<n-tr>
<n-td>每月赞助 X RMB</n-td><n-td>vipX</n-td><n-td>🧩 更多计划视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖)</n-td>
</n-tr>
</n-tbody>
</n-table>
</n-flex>
<n-divider title-placement="center">关于作者</n-divider>
<n-space vertical>
<!-- <h1>关于作者</h1>-->
<n-avatar width="100" src="https://avatars.githubusercontent.com/u/7401917?v=4" />
<h2><a href="https://github.com/ArvinLovegood" target="_blank">@ArvinLovegood</a></h2>
<p>一个热爱编程的小白欢迎关注我的Github</p>
<n-image width="300" src="https://go-stock.sparkmemory.top/assets/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88-DEJtWc_y.png" />
<p>一个热爱编程的小白欢迎关注我的Github/微信公众号</p>
<n-image width="300" :src="wxgzh" />
<p>开源不易如果觉得好用可以请作者喝杯咖啡</p>
<n-flex justify="center">
<n-image width="200" :src="alipay" />

View File

@@ -7,7 +7,7 @@ import {
GetConfig,
GetFollowedFund,
GetfundList,
GetVersionInfo,
GetVersionInfo, OpenURL,
UnFollowFund
} from "../../wailsjs/go/main/App";
import vueDanmaku from 'vue3-danmaku'
@@ -147,8 +147,19 @@ function formatterTitle(title){
function search(code,name){
setTimeout(() => {
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
//window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
//window.open("https://finance.sina.com.cn/fund/quotes/"+code+"/bc.shtml","_blank","width=1000,height=800,top=100,left=100,toolbar=no,location=no")
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
break
default :
OpenURL("https://fund.eastmoney.com/"+code+".html")
}
})
}, 500)
}

View File

@@ -1,5 +1,5 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, ref} from 'vue'
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, ref} from 'vue'
import {
GetAIResponseResult,
GetConfig,
@@ -84,8 +84,6 @@ function getIndex() {
})
}
onBeforeMount(() => {
nowTab.value = route.query.name
stockCode.value = route.query.stockCode
@@ -199,6 +197,7 @@ function getAiSummary() {
summaryModal.value = true
loading.value = true
GetAIResponseResult("市场资讯").then(result => {
loading.value = false
if (result.content) {
aiSummary.value = result.content
question.value = result.question
@@ -311,7 +310,7 @@ function ReFlesh(source) {
<template>
<n-card>
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab" style="--wails-draggable:drag">
<n-tab-pane name="市场快讯" tab="市场快讯">
<n-grid :cols="2" :y-gap="0">
<n-gi>
@@ -389,10 +388,34 @@ function ReFlesh(source) {
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="指标行情" tab="指标行情">
<n-tab-pane name="重大指数" tab="重大指数">
<n-tabs type="segment" animated>
<n-tab-pane name="科创50" tab="科创50">
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
<n-tab-pane name="恒生科技指数" tab="恒生科技指数">
<k-line-chart code="hkHSTECH" :chart-height="panelHeight" name="恒生科技指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="科创50" tab="科创50" >
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="科创芯片" tab="科创芯片" >
<k-line-chart code="sh000685" :chart-height="panelHeight" name="科创芯片" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="证券龙头" tab="证券龙头" >
<k-line-chart code="sz399437" :chart-height="panelHeight" name="证券龙头" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="高端装备" tab="高端装备" >
<k-line-chart code="sz399437" :chart-height="panelHeight" name="高端装备" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证银行" tab="中证银行">
<k-line-chart code="sz399986" :chart-height="panelHeight" name="中证银行" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="上证医药" tab="上证医药">
<k-line-chart code="sh000037" :chart-height="panelHeight" name="上证医药" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="沪深300" tab="沪深300">
@@ -671,5 +694,4 @@ function ReFlesh(source) {
</template>
<style scoped>
</style>

View File

@@ -1,15 +1,15 @@
<script setup>
import {computed, onBeforeUnmount, onMounted, ref} from "vue";
import {h, onBeforeUnmount, onMounted, ref} from "vue";
import {
AddPrompt, DelPrompt,
ExportConfig,
GetConfig,
GetPromptTemplates,
SendDingDingMessageByType,
UpdateConfig
UpdateConfig,CheckSponsorCode
} from "../../wailsjs/go/main/App";
import {useMessage} from "naive-ui";
import {NTag, useMessage} from "naive-ui";
import {data, models} from "../../wailsjs/go/models";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage()
@@ -46,6 +46,7 @@ const formValue = ref({
darkTheme:true,
enableFund:false,
enablePushNews:false,
sponsorCode:"",
})
const promptTemplates=ref([])
onMounted(()=>{
@@ -80,6 +81,8 @@ onMounted(()=>{
formValue.value.darkTheme = res.darkTheme
formValue.value.enableFund = res.enableFund
formValue.value.enablePushNews = res.enablePushNews
formValue.value.sponsorCode = res.sponsorCode
//console.log(res)
})
@@ -120,15 +123,28 @@ function saveConfig(){
enableNews:formValue.value.enableNews,
darkTheme:formValue.value.darkTheme,
enableFund:formValue.value.enableFund,
enablePushNews:formValue.value.enablePushNews
enablePushNews:formValue.value.enablePushNews,
sponsorCode:formValue.value.sponsorCode
})
if (config.sponsorCode){
CheckSponsorCode(config.sponsorCode).then(res=>{
if (res.code){
UpdateConfig(config).then(res=>{
message.success(res)
EventsEmit("updateSettings", config);
})
}else{
message.error(res.msg)
}
})
}else{
UpdateConfig(config).then(res=>{
message.success(res)
EventsEmit("updateSettings", config);
})
}
//console.log("Settings",config)
UpdateConfig(config).then(res=>{
message.success(res)
EventsEmit("updateSettings", config);
})
}
@@ -199,6 +215,7 @@ function importConfig(){
formValue.value.darkTheme = config.darkTheme
formValue.value.enableFund = config.enableFund
formValue.value.enablePushNews = config.enablePushNews
formValue.value.sponsorCode = config.sponsorCode
// formRef.value.resetFields()
};
reader.readAsText(file);
@@ -267,12 +284,13 @@ function deletePrompt(ID){
</script>
<template>
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px;">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" >
<n-grid :cols="24" :x-gap="24" style="text-align: left" :layout-shift-disabled="true">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>
</n-gi>
<n-flex justify="left" style="text-align: left;--wails-draggable:drag" >
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" style="--wails-draggable:no-drag">
<n-card :title="()=> h(NTag, { type: 'primary',bordered:false },()=> '基础设置')" size="small" >
<n-grid :cols="24" :x-gap="24" style="text-align: left" >
<!-- <n-gi :span="24">-->
<!-- <n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>-->
<!-- </n-gi>-->
<n-form-item-gi :span="10" label="Tushare &nbsp;&nbsp;Token" path="tushareToken" >
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
</n-form-item-gi>
@@ -292,15 +310,24 @@ function deletePrompt(ID){
<n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath" >
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
</n-form-item-gi>
<n-form-item-gi :span="6" label="指数基金:" path="enableFund" >
<n-form-item-gi :span="3" label="指数基金:" path="enableFund" >
<n-switch v-model:value="formValue.enableFund" />
</n-form-item-gi>
<n-form-item-gi :span="11" label="赞助码:" path="sponsorCode" >
<n-input-group>
<n-input :show-count="true" placeholder="赞助码" v-model:value="formValue.sponsorCode" />
<n-button type="success" secondary strong @click="CheckSponsorCode(formValue.sponsorCode).then((res) => {message.warning(res.msg) })">验证</n-button>
</n-input-group>
</n-form-item-gi>
</n-grid>
</n-card>
<n-card :title="()=> h(NTag, { type: 'primary',bordered:false },()=> '通知设置')" size="small" >
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi>
<!-- <n-gi :span="24">-->
<!-- <n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>-->
<!-- </n-gi>-->
<n-form-item-gi :span="4" label="钉钉推送:" path="dingPush.enable" >
<n-switch v-model:value="formValue.dingPush.enable" />
</n-form-item-gi>
@@ -321,11 +348,13 @@ function deletePrompt(ID){
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
</n-form-item-gi>
</n-grid>
</n-card>
<n-card :title="()=> h(NTag, { type: 'primary',bordered:false },()=> 'AI设置')" size="small" >
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
</n-gi>
<!-- <n-gi :span="24">-->
<!-- <n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>-->
<!-- </n-gi>-->
<n-form-item-gi :span="3" label="AI诊股" path="openAI.enable" >
<n-switch v-model:value="formValue.openAI.enable" />
</n-form-item-gi>
@@ -350,7 +379,7 @@ function deletePrompt(ID){
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="openAI maxTokens" path="openAI.maxTokens" >
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天)" path="openAI.maxTokens" >
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天)" path="openAI.kDays" >
<n-input-number min="30" step="1" max="365" placeholder="日K线数据(天)" title="天数越多消耗tokens越多" v-model:value="formValue.openAI.kDays"/>
</n-form-item-gi>
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型系统 Prompt" path="openAI.prompt" >
@@ -360,7 +389,7 @@ function deletePrompt(ID){
placeholder="请输入系统prompt"
:autosize="{
minRows: 5,
maxRows: 8
maxRows: 6
}"
/>
</n-form-item-gi>
@@ -371,33 +400,43 @@ function deletePrompt(ID){
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
:autosize="{
minRows: 5,
maxRows: 8
maxRows: 6
}"
/>
</n-form-item-gi>
</n-grid>
<n-gi :span="24">
<n-grid :cols="24">
<n-gi :span="24">
<n-space justify="center">
<n-button type="warning" @click="managePrompts">
<n-button type="warning" @click="managePrompts">
添加提示词模板
</n-button>
<n-button type="primary" @click="saveConfig">
保存
</n-button>
<n-button type="info" @click="exportConfig">
导出
</n-button>
<n-button type="error" @click="importConfig">
导入
</n-button>
<n-button type="primary" @click="saveConfig">
保存
</n-button>
<n-button type="info" @click="exportConfig">
导出
</n-button>
<n-button type="error" @click="importConfig">
导入
</n-button>
</n-space>
</n-gi>
</n-gi>
<n-gi :span="24" v-if="promptTemplates.length>0" type="warning">
<n-flex justify="start" style="margin-top: 4px" >
<n-text type="warning" >
<n-flex justify="left" >
<n-tag :bordered="false" type="warning" > 提示词模板:</n-tag>
<n-tag size="medium" secondary v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" closable @close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content"
:type="prompt.type==='模型系统Prompt'?'success':'info'" :bordered="false"> {{ prompt.name }}
</n-tag>
</n-flex>
</n-text>
</n-flex>
</n-gi>
</n-grid>
</n-card>
</n-form>
<n-gi :span="24" v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" >
<n-flex justify="start">
<n-tag closable @close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content" :type="prompt.type==='模型系统Prompt'?'success':'info'" :bordered="false"> {{prompt.name}} </n-tag>
</n-flex>
</n-gi>
</n-flex>
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
<n-card
@@ -442,5 +481,9 @@ function deletePrompt(ID){
</template>
<style scoped>
.cardHeaderClass{
font-size: 16px;
font-weight: bold;
color: red;
}
</style>

View File

@@ -26,7 +26,10 @@ import {
SetStockAICron,
SetStockSort,
ShareAnalysis,
UnFollow
UnFollow,
OpenURL,
SaveImage,
SaveWordFile
} from '../../wailsjs/go/main/App'
import {
NAvatar,
@@ -41,6 +44,7 @@ import {
useNotification
} from 'naive-ui'
import {
Environment,
EventsEmit,
EventsOff,
EventsOn,
@@ -64,6 +68,7 @@ import {keys, padStart} from "lodash";
import {useRoute, useRouter} from 'vue-router'
import MoneyTrend from "./moneyTrend.vue";
import {TaskTools} from "@vicons/carbon";
import StockSparkLine from "./stockSparkLine.vue";
const route = useRoute()
const router = useRouter()
@@ -102,7 +107,7 @@ const modalShow3 = ref(false)
const modalShow4 = ref(false)
const modalShow5 = ref(false)
const addBTN = ref(true)
const enableTools= ref(false)
const enableTools = ref(false)
const formModel = ref({
name: "",
code: "",
@@ -383,7 +388,15 @@ EventsOn("updateVersion", async (msg) => {
type: 'primary',
size: 'small',
onClick: () => {
window.open(msg.html_url)
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(msg.html_url)
break
default :
OpenURL(msg.html_url)
}
})
}
}, {default: () => '查看'})
}
@@ -440,7 +453,7 @@ function AddStock() {
Follow(data.code).then(result => {
if (result === "关注成功") {
if (data.code.startsWith("us")) {
data.code= "gb_" + data.code.replace("us", "").toLowerCase()
data.code = "gb_" + data.code.replace("us", "").toLowerCase()
}
stocks.value.push(data.code)
message.success(result)
@@ -613,12 +626,28 @@ function onSelect(item) {
function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
Environment().then(env => {
switch (env.platform) {
case 'windows':
window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
)
break
default :
OpenURL(url)
break
}
})
return window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
);
//
// return window.open(
// url,
// 'centeredWindow',
// `width=${width},height=${height},left=${left},top=${top}`
// );
}
function search(code, name) {
@@ -630,7 +659,7 @@ function search(code, name) {
//window.open("https://www.iwencai.com/unifiedwap/result?w=" + name)
//window.open("https://www.iwencai.com/chat/?question="+code)
openCenteredWindow("https://www.iwencai.com/unifiedwap/result?w=" + name,1000,800)
openCenteredWindow("https://www.iwencai.com/unifiedwap/result?w=" + name, 1000, 800)
}, 500)
}
@@ -1358,7 +1387,7 @@ function aiReCheckStock(stock, stockCode) {
//
//message.info("sysPromptId:"+data.sysPromptId)
NewChatStream(stock, stockCode, data.question, data.sysPromptId,enableTools.value)
NewChatStream(stock, stockCode, data.question, data.sysPromptId, enableTools.value)
}
function aiCheckStock(stock, stockCode) {
@@ -1436,21 +1465,42 @@ window.onerror = function (msg, source, lineno, colno, error) {
};
function saveAsImage(name, code) {
const element = document.querySelector('.md-editor-preview');
if (element) {
html2canvas(element, {
useCORS: true, // 解决跨域图片问题
scale: 2, // 提高截图质量
allowTaint: true, // 允许跨域图片
}).then(canvas => {
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = name + "[" + code + ']-ai-analysis-result.png';
link.click();
});
} else {
message.error('无法找到分析结果元素');
}
Environment().then(env => {
switch (env.platform) {
case 'windows':
const element = document.querySelector('.md-editor-preview');
if (element) {
html2canvas(element, {
useCORS: true, // 解决跨域图片问题
scale: 2, // 提高截图质量
allowTaint: true, // 允许跨域图片
}).then(canvas => {
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = name + "[" + code + ']-ai-analysis-result.png';
link.click();
});
} else {
message.error('无法找到分析结果元素');
}
break
default :
saveCanvasImage(name)
}
})
}
async function saveCanvasImage(name) {
const element = document.querySelector('.md-editor-preview'); // 要截图的 DOM 节点
const canvas = await html2canvas(element)
const dataUrl = canvas.toDataURL('image/png') // base64 格式
const base64 = dataUrl.replace(/^data:image\/png;base64,/, '')
// 调用 Go 后端保存文件Wails 绑定方法)
await SaveImage(name,base64).then(result => {
message.success(result)
})
}
async function copyToClipboard() {
@@ -1510,13 +1560,26 @@ AI赋能股票分析自选股行情获取成本盈亏展示涨跌报警
`
// landscape就是横着的portrait是竖着的默认是竖屏portrait。
const blob = await asBlob(value, {orientation: 'portrait'})
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = `${data.name}[${data.code}]-ai-analysis-result.docx`;
a.click()
// 下载后将标签移除
URL.revokeObjectURL(a.href);
a.remove()
const { platform } = await Environment()
switch (platform) {
case 'windows':
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = `${data.name}[${data.code}]-ai-analysis-result.docx`;
a.click()
// 下载后将标签移除
URL.revokeObjectURL(a.href);
a.remove()
break
default:
const arrayBuffer = await blob.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
const binary = uint8Array.reduce((data, byte) => data + String.fromCharCode(byte), '')
const base64 = btoa(binary)
await SaveWordFile(`${data.name}[${data.code}]-ai-analysis-result.docx`, base64).then(result => {
message.success(result)
})
}
}
function share(code, name) {
@@ -1746,7 +1809,8 @@ function searchStockReport(stockCode) {
取消关注
</n-button>&nbsp;
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning"
@click="aiCheckStock(result['股票名称'],result['股票代码'])">
AI分析
</n-button>
</template>
@@ -1755,7 +1819,9 @@ function searchStockReport(stockCode) {
<n-text :type="'info'">{{ result["日期"] + " " + result["时间"] }}</n-text>
<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 + " ¥ )" }}
{{
"成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )"
}}
</n-tag>
</n-flex>
</template>
@@ -1796,8 +1862,8 @@ function searchStockReport(stockCode) {
<n-card :data-sort="result.sort" :id="result['股票代码']" :data-code="result['股票代码']" :bordered="true"
:title="result['股票名称']" :closable="false"
@close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
<n-grid :cols="1" :y-gap="6">
<n-gi>
<n-grid :cols="12" :y-gap="6">
<n-gi :span="6">
<n-text :type="result.type">
<n-number-animation :duration="1000" :precision="2" :from="result['上次当前价格']"
:to="Number(result['当前价格'])"/>
@@ -1813,6 +1879,10 @@ function searchStockReport(stockCode) {
<n-number-animation :duration="1000" :precision="2" :from="0" :to="result.profitAmountToday"/>
</n-text>
</n-gi>
<n-gi :span="6">
<stock-spark-line :last-price="Number(result['当前价格'])" :open-price="Number(result['昨日收盘价'])"
:stock-code="result['股票代码']" :stock-name="result['股票名称']"></stock-spark-line>
</n-gi>
</n-grid>
<n-grid :cols="2" :y-gap="4" :x-gap="4">
<n-gi>
@@ -1882,9 +1952,10 @@ function searchStockReport(stockCode) {
取消关注
</n-button>&nbsp;
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
AI分析
</n-button>
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning"
@click="aiCheckStock(result['股票名称'],result['股票代码'])">
AI分析
</n-button>
<n-button secondary type="error" size="tiny"
@click="delStockGroup(result['股票代码'],result['股票名称'],group.ID)">移出分组
</n-button>
@@ -1894,7 +1965,9 @@ function searchStockReport(stockCode) {
<n-text :type="'info'">{{ result["日期"] + " " + result["时间"] }}</n-text>
<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 + " ¥ )" }}
{{
"成本:" + result.costPrice + "*" + result.costVolume + " " + result.profit + "%" + " ( " + result.profitAmount + " ¥ )"
}}
</n-tag>
</n-flex>
</template>
@@ -2083,7 +2156,9 @@ function searchStockReport(stockCode) {
不启用AI函数工具调用
</template>
</n-switch>
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens</n-gradient-text>
<n-gradient-text type="error" style="margin-left: 10px">
*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens
</n-gradient-text>
</n-flex>
<n-flex justify="space-between" style="margin-bottom: 10px">
<n-select style="width: 49%" v-model:value="data.sysPromptId" label-field="name" value-field="ID"

View File

@@ -0,0 +1,137 @@
<script setup>
import {onMounted, onBeforeMount, ref, watchEffect} from "vue";
import * as echarts from 'echarts';
import {GetStockMinutePriceLineData} from "../../wailsjs/go/main/App"; // 如果您使用多个组件,请将此样式导入放在您的主文件中
const {stockCode,stockName,lastPrice,openPrice,darkTheme} = defineProps({
stockCode: {
type: String,
default: ""
},
stockName: {
type: String,
default: ""
},
lastPrice: {
type: Number,
default: 0
},
openPrice: {
type: Number,
default: 0
},
darkTheme: {
type: Boolean,
default: true
},
})
const chartRef=ref();
function setChartData(chart) {
//console.log("setChartData")
GetStockMinutePriceLineData(stockCode, stockName).then(result => {
//console.log("GetStockMinutePriceLineData",result)
const priceData = result.priceData
let category = []
let price = []
let min = 0
let max = 0
for (let i = 0; i < priceData.length; i++) {
category.push(priceData[i].time)
price.push(priceData[i].price)
if (min === 0 || min > priceData[i].price) {
min = priceData[i].price
}
if (max < priceData[i].price) {
max = priceData[i].price
}
}
let option = {
padding: [0, 0, 0, 0],
grid: {
top: 0,
left: 0,
right: 0,
bottom: 0
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
xAxis: {
show: false,
type: 'category',
data: category
},
yAxis: {
show: false,
type: 'value',
min: (min).toFixed(2),
max: (max).toFixed(2),
minInterval: 0.01,
},
// visualMap: {
// show: false,
// type: 'piecewise',
// pieces: [
// {
// min: Number(min),
// max: Number(openPrice),
// color: 'green'
// },
// {
// min: Number(openPrice),
// max: Number(max),
// color: 'red'
// }
// ]
// },
series: [
{
data: price,
type: 'line',
smooth: false,
stack: '总量',
showSymbol: false,
lineStyle: {
color: lastPrice > openPrice ? 'rgba(245, 0, 0, 1)' : 'rgb(6,251,10)'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: lastPrice > openPrice ? 'rgba(245, 0, 0, 1)' : 'rgba(6,251,10, 1)'
}, {
offset: 1,
color: lastPrice > openPrice ? 'rgba(245, 0, 0, 0.25)' : 'rgba(6,251,10, 0.25)'
}])
},
}
]
};
chart.setOption(option);
})
}
const chart =ref( null)
onMounted(() => {
chart.value = echarts.init( document.getElementById('sparkLine'+stockCode));
setChartData(chart.value);
})
watchEffect(() => {
console.log(stockName,'lastPrice变化为:', lastPrice,lastPrice > openPrice)
setChartData(chart.value);
})
</script>
<template>
<div style="height: 20px;width: 100%" :id="'sparkLine'+stockCode">
</div>
</template>

View File

@@ -1,25 +1,26 @@
<script setup lang="ts">
import {h} from 'vue'
import {NTag,NImage} from 'naive-ui'
import EmbeddedUrl from "./EmbeddedUrl.vue";
</script>
<template>
<n-tabs type="line" animated>
<n-tab-pane name="百度股市通" tab="百度股市通">
<embedded-url url="https://gushitong.baidu.com" :height="'calc(100vh - 252px)'"/>
<n-tab-pane name="选股通" tab="选股通">
<embedded-url url="https://xuangutong.com.cn" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<!-- <n-tab-pane name="百度股市通" tab="百度股市通">-->
<!-- <embedded-url url="https://gushitong.baidu.com" :height="'calc(100vh - 252px)'"/>-->
<!-- </n-tab-pane>-->
<n-tab-pane name="东财大盘星图" tab="东财大盘星图">
<embedded-url url="https://quote.eastmoney.com/stockhotmap/" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="TopHub" tab="TopHub(今日热榜)">
<embedded-url url="https://tophub.today/c/finance" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="选股通" tab="选股通">
<embedded-url url="https://xuangutong.com.cn" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<n-tab-pane name="摸鱼" tab="摸鱼">
<embedded-url url="https://996.ninja/" :height="'calc(100vh - 252px)'"/>
</n-tab-pane>
<!-- <n-tab-pane name="摸鱼" tab="摸鱼">-->
<!-- <embedded-url url="https://996.ninja/" :height="'calc(100vh - 252px)'"/>-->
<!-- </n-tab-pane>-->
<n-tab-pane name="欢迎推荐更多有趣的财经网页" tab="欢迎推荐更多有趣的财经网页">

View File

@@ -2,21 +2,22 @@ import {createMemoryHistory, createRouter, createWebHashHistory, createWebHistor
import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue'
import about from "../components/about.vue";
import aboutView from "../components/about.vue";
import fundView from "../components/fund.vue";
import market from "../components/market.vue";
import marketView from "../components/market.vue";
const routes = [
{ path: '/', component: stockView,name: 'stock'},
{ path: '/fund', component: fundView,name: 'fund' },
{ path: '/settings', component: settingsView,name: 'settings' },
{ path: '/about', component: about,name: 'about' },
{ path: '/market', component: market,name: 'market' },
{ path: '/about', component: aboutView,name: 'about' },
{ path: '/market', component: marketView,name: 'market' },
]
const router = createRouter({
history: createWebHistory(),
//history: createWebHistory(),
history: createWebHashHistory(),
routes,
})

View File

@@ -2,6 +2,7 @@
// This file is automatically generated. DO NOT EDIT
import {data} from '../models';
import {models} from '../models';
import {context} from '../models';
export function AddCronTask(arg1:data.FollowedStock):Promise<any>;
@@ -13,7 +14,11 @@ export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
export function CheckUpdate():Promise<void>;
export function CheckSponsorCode(arg1:string):Promise<Record<string, any>>;
export function CheckStockBaseInfo(arg1:context.Context):Promise<void>;
export function CheckUpdate(arg1:number):Promise<void>;
export function ClsCalendar():Promise<Array<any>>;
@@ -49,6 +54,8 @@ export function GetMoneyRankSina(arg1:string):Promise<Array<Record<string, any>>
export function GetPromptTemplates(arg1:string,arg2:string):Promise<any>;
export function GetSponsorInfo():Promise<Record<string, any>>;
export function GetStockCommonKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
export function GetStockKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
@@ -85,6 +92,8 @@ export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any,arg5:
export function NewsPush(arg1:any):Promise<void>;
export function OpenURL(arg1:string):Promise<void>;
export function ReFleshTelegraphList(arg1:string):Promise<any>;
export function RemoveGroup(arg1:number):Promise<string>;
@@ -95,6 +104,10 @@ export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:st
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
export function SaveImage(arg1:string,arg2:string):Promise<string>;
export function SaveWordFile(arg1:string,arg2:string):Promise<string>;
export function SearchStock(arg1:string):Promise<Record<string, any>>;
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;

View File

@@ -22,8 +22,16 @@ export function AnalyzeSentiment(arg1) {
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
}
export function CheckUpdate() {
return window['go']['main']['App']['CheckUpdate']();
export function CheckSponsorCode(arg1) {
return window['go']['main']['App']['CheckSponsorCode'](arg1);
}
export function CheckStockBaseInfo(arg1) {
return window['go']['main']['App']['CheckStockBaseInfo'](arg1);
}
export function CheckUpdate(arg1) {
return window['go']['main']['App']['CheckUpdate'](arg1);
}
export function ClsCalendar() {
@@ -94,6 +102,10 @@ export function GetPromptTemplates(arg1, arg2) {
return window['go']['main']['App']['GetPromptTemplates'](arg1, arg2);
}
export function GetSponsorInfo() {
return window['go']['main']['App']['GetSponsorInfo']();
}
export function GetStockCommonKLine(arg1, arg2, arg3) {
return window['go']['main']['App']['GetStockCommonKLine'](arg1, arg2, arg3);
}
@@ -166,6 +178,10 @@ export function NewsPush(arg1) {
return window['go']['main']['App']['NewsPush'](arg1);
}
export function OpenURL(arg1) {
return window['go']['main']['App']['OpenURL'](arg1);
}
export function ReFleshTelegraphList(arg1) {
return window['go']['main']['App']['ReFleshTelegraphList'](arg1);
}
@@ -186,6 +202,14 @@ export function SaveAsMarkdown(arg1, arg2) {
return window['go']['main']['App']['SaveAsMarkdown'](arg1, arg2);
}
export function SaveImage(arg1, arg2) {
return window['go']['main']['App']['SaveImage'](arg1, arg2);
}
export function SaveWordFile(arg1, arg2) {
return window['go']['main']['App']['SaveWordFile'](arg1, arg2);
}
export function SearchStock(arg1) {
return window['go']['main']['App']['SearchStock'](arg1);
}

View File

@@ -343,6 +343,7 @@ export namespace data {
browserPoolSize: number;
enableFund: boolean;
enablePushNews: boolean;
sponsorCode: string;
static createFrom(source: any = {}) {
return new Settings(source);
@@ -379,6 +380,7 @@ export namespace data {
this.browserPoolSize = source["browserPoolSize"];
this.enableFund = source["enableFund"];
this.enablePushNews = source["enablePushNews"];
this.sponsorCode = source["sponsorCode"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -424,6 +426,8 @@ export namespace data {
is_hs: string;
act_name: string;
act_ent_type: string;
bk_name: string;
bk_code: string;
static createFrom(source: any = {}) {
return new StockBasic(source);
@@ -452,6 +456,8 @@ export namespace data {
this.is_hs = source["is_hs"];
this.act_name = source["act_name"];
this.act_ent_type = source["act_ent_type"];
this.bk_name = source["bk_name"];
this.bk_code = source["bk_code"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -700,6 +706,7 @@ export namespace models {
icon: string;
alipay: string;
wxpay: string;
wxgzh: string;
buildTimeStamp: number;
officialStatement: string;
IsDel: number;
@@ -719,6 +726,7 @@ export namespace models {
this.icon = source["icon"];
this.alipay = source["alipay"];
this.wxpay = source["wxpay"];
this.wxgzh = source["wxgzh"];
this.buildTimeStamp = source["buildTimeStamp"];
this.officialStatement = source["officialStatement"];
this.IsDel = source["IsDel"];

3
go.mod
View File

@@ -13,6 +13,7 @@ require (
github.com/go-ego/gse v0.80.3
github.com/go-resty/resty/v2 v2.16.2
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
github.com/robertkrimen/otto v0.5.1
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.49.1
@@ -20,6 +21,7 @@ require (
github.com/tidwall/gjson v1.14.2
github.com/wailsapp/wails/v2 v2.10.1
go.uber.org/zap v1.27.0
golang.org/x/net v0.38.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
@@ -81,7 +83,6 @@ require (
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.38.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect

2
go.sum
View File

@@ -57,6 +57,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=

80
main.go
View File

@@ -9,7 +9,6 @@ import (
"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"
@@ -19,10 +18,8 @@ import (
log "go-stock/backend/logger"
"go-stock/backend/models"
"os"
goruntime "runtime"
"runtime/debug"
"strings"
"time"
)
//go:embed frontend/dist
@@ -40,6 +37,9 @@ var alipay []byte
//go:embed build/screenshot/wxpay.jpg
var wxpay []byte
//go:embed build/screenshot/扫码_搜索联合传播样式-白色版.png
var wxgzh []byte
//go:embed build/stock_basic.json
var stocksBin []byte
@@ -54,6 +54,7 @@ var stocksBinUS []byte
var Version string
var VersionCommit string
var OFFICIAL_STATEMENT string
var BuildKey string
func main() {
checkDir("data")
@@ -68,34 +69,33 @@ func main() {
// 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)
})
if IsMacOS() {
AppMenu.Append(menu.EditMenu())
}
//FileMenu := AppMenu.AddSubmenu("设置")
//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)
//})
//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()
//if goruntime.GOOS == "windows" {
// FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("z"), func(_ *menu.CallbackData) {
// runtime.WindowHide(app.ctx)
// })
//}
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
// runtime.Quit(app.ctx)
@@ -106,7 +106,7 @@ func main() {
//var width, height int
//var err error
//
width, _, err := getScreenResolution()
width, _, minWidth, minHeight, err := getScreenResolution()
if err != nil {
log.SugaredLogger.Error("get screen resolution error")
width = 1456
@@ -119,18 +119,20 @@ func main() {
backgroundColour = &options.RGBA{R: 27, G: 38, B: 54, A: 1}
}
//frameless := getFrameless()
// Create application with options
err = wails.Run(&options.App{
Title: "go-stock",
Title: "go-stockAI赋能股票分析✨",
Width: width * 4 / 5,
Height: 900,
MinWidth: 1456,
MinHeight: 768,
MinWidth: minWidth,
MinHeight: minHeight,
//MaxWidth: width,
//MaxHeight: height,
DisableResize: false,
Fullscreen: false,
Frameless: true,
Frameless: false,
StartHidden: false,
HideWindowOnClose: false,
EnableDefaultContextMenu: true,
@@ -163,12 +165,11 @@ func main() {
// Mac platform specific options
Mac: &mac.Options{
TitleBar: &mac.TitleBar{
TitlebarAppearsTransparent: true,
TitlebarAppearsTransparent: false,
HideTitle: false,
HideTitleBar: false,
FullSizeContent: false,
UseToolbar: false,
HideToolbarSeparator: true,
UseToolbar: true,
},
Appearance: mac.NSAppearanceNameDarkAqua,
WebviewIsTransparent: true,
@@ -346,6 +347,9 @@ func checkDir(dir string) {
os.Mkdir(dir, os.ModePerm)
log.SugaredLogger.Info("create dir: " + dir)
}
if BuildKey == "" {
BuildKey = "cc1e0d684e32f176c56ff1fcf384dcd9"
}
}
// PanicHandler 捕获 panic 的包装函数

23
utils.go Normal file
View File

@@ -0,0 +1,23 @@
package main
// @Author spark
// @Date 2025/7/8 18:51
// @Desc
//-----------------------------------------------------------------------------------
import "runtime"
// IsWindows 判断是否为 Windows 系统
func IsWindows() bool {
return runtime.GOOS == "windows"
}
// IsMacOS 判断是否为 macOS 系统
func IsMacOS() bool {
return runtime.GOOS == "darwin"
}
// IsLinux 判断是否为 Linux 系统
func IsLinux() bool {
return runtime.GOOS == "linux"
}