Compare commits
228 Commits
v2025.4.1.
...
dev-single
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1a9a8d4d8 | ||
|
|
b98f829286 | ||
|
|
dda160069a | ||
|
|
f80ea181be | ||
|
|
f5c8f5d0ef | ||
|
|
23d3566f31 | ||
|
|
052104b43a | ||
|
|
93e8fb27b5 | ||
|
|
25623d90d7 | ||
|
|
8db94da233 | ||
|
|
60e7d87918 | ||
|
|
615b4d231a | ||
|
|
490a3c0847 | ||
|
|
38f83674ef | ||
|
|
d26c4bc986 | ||
|
|
7e919376b5 | ||
|
|
1d9ef724e6 | ||
|
|
8e982d4430 | ||
|
|
a67559831a | ||
|
|
9718d3311d | ||
|
|
789e7427ce | ||
|
|
801aa14c7a | ||
|
|
f5c621fbcc | ||
|
|
119f0f8aa7 | ||
|
|
fe814974fd | ||
|
|
dd3c231637 | ||
|
|
e05ff94aba | ||
|
|
bbd4bb5b48 | ||
|
|
58f3009902 | ||
|
|
c6b841fb8f | ||
|
|
2b28390414 | ||
|
|
7887dfed5e | ||
|
|
a4c98933a4 | ||
|
|
ad63ffff7f | ||
|
|
1ccc2f8b1f | ||
|
|
dc5483aa07 | ||
|
|
8c82ba4a38 | ||
|
|
fd905ff278 | ||
|
|
6ec0f5fbe0 | ||
|
|
32706fb4dc | ||
|
|
2cb661734f | ||
|
|
4fab910340 | ||
|
|
84e4ba8474 | ||
|
|
76a44fae32 | ||
|
|
7ea974f1a6 | ||
|
|
7ea160b6b5 | ||
|
|
c2f260c613 | ||
|
|
2d224ccfc4 | ||
|
|
a66f2156f1 | ||
|
|
e90727773f | ||
|
|
89dcb713be | ||
|
|
6f4b21207d | ||
|
|
f51e3d863a | ||
|
|
c180c2a5f8 | ||
|
|
3ba18e8ef2 | ||
|
|
f0314187e5 | ||
|
|
6440885688 | ||
|
|
2dd4f072b2 | ||
|
|
b5843bcdb8 | ||
|
|
c65d0b79f4 | ||
|
|
92bb0097cd | ||
|
|
0c6bd7292e | ||
|
|
eeae1f77f4 | ||
|
|
707e353ea8 | ||
|
|
5ee14b703c | ||
|
|
04a46108f3 | ||
|
|
48a601f776 | ||
|
|
e249933f8b | ||
|
|
a8bb2b5399 | ||
|
|
16c89de792 | ||
|
|
71bfed3744 | ||
|
|
edd1bf94f9 | ||
|
|
cfe1abb07f | ||
|
|
8b94e14ec9 | ||
|
|
44e1093e8e | ||
|
|
5e7f34652a | ||
|
|
5b9a81d770 | ||
|
|
7021a59ee6 | ||
|
|
433dea0772 | ||
|
|
378a5c47ba | ||
|
|
9a60736739 | ||
|
|
efe6365ea5 | ||
|
|
062df80712 | ||
|
|
528482db48 | ||
|
|
746e5ec98a | ||
|
|
6d345ae91d | ||
|
|
888a97e4d3 | ||
|
|
ebeaf104bb | ||
|
|
b945a0e0e1 | ||
|
|
111252f8bd | ||
|
|
2e5ec6ace8 | ||
|
|
3e16574faa | ||
|
|
482472af4e | ||
|
|
bdc3689ac8 | ||
|
|
e8ebb577b2 | ||
|
|
71f8265bc2 | ||
|
|
43063fa7fb | ||
|
|
86f041b4d6 | ||
|
|
0ce7e8e7a7 | ||
|
|
bbab60e2ad | ||
|
|
1fbd564bff | ||
|
|
f0ad50303e | ||
|
|
55839d3329 | ||
|
|
3f4cbca4a7 | ||
|
|
6e3b9ff1f9 | ||
|
|
0e45866421 | ||
|
|
e0225c4158 | ||
|
|
2f6c17fb2a | ||
|
|
22b4fcdffb | ||
|
|
7dd10d443e | ||
|
|
5b5590ebd7 | ||
|
|
be02343d68 | ||
|
|
942d249671 | ||
|
|
9f2719cdbc | ||
|
|
0343a95a21 | ||
|
|
9337084ebf | ||
|
|
18834d9281 | ||
|
|
9e06136983 | ||
|
|
30a3d1d9ef | ||
|
|
5b6de9f9f6 | ||
|
|
65d737c695 | ||
|
|
af73691b22 | ||
|
|
b2c12cffbb | ||
|
|
a936dc6371 | ||
|
|
f6d217e4fd | ||
|
|
378b669827 | ||
|
|
0d3fd47552 | ||
|
|
a2fee361e7 | ||
|
|
1ef950b961 | ||
|
|
934b4608b7 | ||
|
|
68e7b6a68c | ||
|
|
700572567e | ||
|
|
c9ade36844 | ||
|
|
0a2491d725 | ||
|
|
9d8af191c5 | ||
|
|
6382be6b19 | ||
|
|
0cafcb9cd4 | ||
|
|
21c7f5390c | ||
|
|
02db6c2e87 | ||
|
|
2811786bfd | ||
|
|
9aa2c4095a | ||
|
|
ad9bea4c24 | ||
|
|
4f8d84b8a0 | ||
|
|
e238700333 | ||
|
|
6bdff0a0f3 | ||
|
|
c7655d2adf | ||
|
|
8996ddf986 | ||
|
|
329936568f | ||
|
|
0d85e24595 | ||
|
|
b266281bbd | ||
|
|
ace3ff7302 | ||
|
|
60b7cdc761 | ||
|
|
3cc597d361 | ||
|
|
68bcfc679a | ||
|
|
78f7808f1b | ||
|
|
d6c3a6b98b | ||
|
|
3b25aa79bb | ||
|
|
e49545a581 | ||
|
|
1185af5a87 | ||
|
|
152a6335d8 | ||
|
|
338e371190 | ||
|
|
3ffcaa0374 | ||
|
|
ed9d9cde77 | ||
|
|
673d446b05 | ||
|
|
e2e0ef2aad | ||
|
|
a8ecbf9329 | ||
|
|
9eded54d8d | ||
|
|
c1d458e5cf | ||
|
|
7158e405a6 | ||
|
|
d993a5525f | ||
|
|
6af6d989ba | ||
|
|
0b3acd9adc | ||
|
|
013de869f4 | ||
|
|
1b67e20932 | ||
|
|
8b510bce94 | ||
|
|
71676eead4 | ||
|
|
2a274db7ae | ||
|
|
4fd5cbf8e6 | ||
|
|
d7b17b2561 | ||
|
|
ad92c41d08 | ||
|
|
47dbbb8813 | ||
|
|
ae9f4073dc | ||
|
|
c7e37e039e | ||
|
|
99b6586c77 | ||
|
|
7e24424ea0 | ||
|
|
58d93c76f6 | ||
|
|
df989b706b | ||
|
|
cf537ca695 | ||
|
|
11a1a47eca | ||
|
|
338064e536 | ||
|
|
8ba26b6250 | ||
|
|
54138ff61e | ||
|
|
d8d5091709 | ||
|
|
7f204ee80d | ||
|
|
4a367b6027 | ||
|
|
e615fc4108 | ||
|
|
2b982f924e | ||
|
|
24e24f8236 | ||
|
|
e77c23e42a | ||
|
|
ef6228922e | ||
|
|
c4caea5be8 | ||
|
|
3535ba57ab | ||
|
|
cedff896bb | ||
|
|
ffc212abc3 | ||
|
|
7bacbe0d89 | ||
|
|
6be23d6abc | ||
|
|
3a74e0ed98 | ||
|
|
4b0b3c0491 | ||
|
|
2bd63cf2f4 | ||
|
|
71d8822d15 | ||
|
|
db3594af77 | ||
|
|
c7d728e613 | ||
|
|
3ca2eed575 | ||
|
|
8cd55034c3 | ||
|
|
344c43cbf1 | ||
|
|
8c49b00057 | ||
|
|
51cc21107a | ||
|
|
ece40d1fc0 | ||
|
|
1a3c8b4fae | ||
|
|
09d3a16841 | ||
|
|
65bc8cde47 | ||
|
|
b45d5dc762 | ||
|
|
512f9a0757 | ||
|
|
9e5650617b | ||
|
|
bac10a2a04 | ||
|
|
65060a91ce | ||
|
|
2ae3893325 | ||
|
|
fdaa80777d |
16
.github/workflows/main.yml
vendored
16
.github/workflows/main.yml
vendored
@@ -4,11 +4,14 @@ on:
|
||||
push:
|
||||
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:
|
||||
@@ -23,6 +26,9 @@ jobs:
|
||||
# - name: 'go-stock-linux-amd64'
|
||||
# platform: 'linux/amd64'
|
||||
# os: 'ubuntu-latest'
|
||||
- name: 'go-stock-darwin-universal'
|
||||
platform: 'darwin/universal'
|
||||
os: 'macos-latest'
|
||||
|
||||
runs-on: ${{ matrix.build.os }}
|
||||
steps:
|
||||
@@ -38,13 +44,15 @@ jobs:
|
||||
echo "::set-output name=commit_message::$commit_message"
|
||||
|
||||
- name: Build wails x go-stock
|
||||
uses: ArvinLovegood/wails-build-action@v3.4
|
||||
uses: ArvinLovegood/wails-build-action@v3.6
|
||||
id: build
|
||||
with:
|
||||
build-name: ${{ matrix.build.name }}
|
||||
build-platform: ${{ matrix.build.platform }}
|
||||
package: true
|
||||
go-version: '1.23'
|
||||
go-version: '1.24'
|
||||
build-tags: ${{ github.ref_name }}
|
||||
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
|
||||
node-version: '18.x'
|
||||
build-statement: ${{ env.OFFICIAL_STATEMENT }}
|
||||
build-key: ${{ env.BUILD_KEY }}
|
||||
node-version: '20.x'
|
||||
|
||||
80
README.md
80
README.md
@@ -1,16 +1,18 @@
|
||||
# go-stock : 基于Wails和NaiveUI构建的AI赋能股票分析工具
|
||||
# go-stock : 基于大语言模型的AI赋能股票分析工具
|
||||
## 
|
||||

|
||||
[](https://github.com/ArvinLovegood/go-stock)
|
||||
[](https://gitee.com/arvinlovegood_admin/go-stock)
|
||||
[](https://gitcode.com/ArvinLovegood/go-stock)
|
||||
|
||||
[//]: # ([](https://gitcode.com/ArvinLovegood/go-stock))
|
||||
|
||||
### 🌟公众号
|
||||

|
||||
|
||||
### 📈 交流群
|
||||
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](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)
|
||||
|
||||
### ✨ 简介
|
||||
- 本项目基于Wails和NaiveUI开发,结合AI大模型构建的股票分析工具。
|
||||
@@ -22,37 +24,75 @@ QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http
|
||||
### 📦 立即体验
|
||||
- 安装版:[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)
|
||||
|
||||
|
||||
### 💬 支持大模型/平台
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |-----------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
||||
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
|
||||
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk),[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) |
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
||||
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
|
||||
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk),[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) ,[优云智算](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock) |
|
||||
|
||||
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
|
||||
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- 火山方舟:每个模型注册即送50万tokens,[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
|
||||
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究,[注册链接](https://tushare.pro/register?reg=701944)
|
||||
- 优云智算(by UCloud):万卡规模4090免费用10小时,新人注册另增50万tokens,海量热门源项目镜像一键部署,[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
|
||||
- 火山方舟:新用户每个模型注册即送50万tokens,[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
|
||||
- 硅基流动(siliconflow),注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意:Tushare只需要120积分即可,注册完成个人资料补充即可得120积分!!!),[注册链接](https://tushare.pro/register?reg=701944)
|
||||
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
|
||||
- 欢迎大家提出宝贵的建议,欢迎提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广告推广💖) |
|
||||
|
||||
## 🧩 重大功能开发计划
|
||||
| 功能说明 | 状态 | 备注 |
|
||||
|-----------------|----|----------------------------------------------------------------------------------------------------------|
|
||||
| 股票分析知识库 | 🚧 | 未来计划 |
|
||||
| Ai智能选股 | 🚧 | Ai智能选股功能开发中(下半年重点开发计划) |
|
||||
| ETF支持 | 🚧 | ETF数据支持 (目前可以查看净值和估值) |
|
||||
| 美股支持 | ✅ | 美股数据支持 |
|
||||
| 港股支持 | ✅ | 港股数据支持 (目前有延迟) |
|
||||
| 美股支持 | ✅ | 美股数据支持 |
|
||||
| 港股支持 | ✅ | 港股数据支持 |
|
||||
| 多轮对话 | ✅ | AI分析后可继续对话提问 |
|
||||
| 自定义AI分析提问模板 | ✅ | 可配置的提问模板 [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha) |
|
||||
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
|
||||
|
||||
## 👀 更新日志
|
||||
### 2025.07.08 实现软件自动更新功能
|
||||
### 2025.07.07 卡片添加迷你分时图
|
||||
### 2025.07.05 MacOs支持
|
||||
### 2025.07.01 AI分析集成工具函数,AI分析将更加智能
|
||||
### 2025.06.30 添加指标选股功能
|
||||
### 2025.06.27 添加财经日历和重大事件时间轴功能
|
||||
### 2025.06.25 添加热门股票、事件和话题功能
|
||||
### 2025.06.18 更新内置股票基础数据,软件内实时市场资讯信息提醒,添加行业研究功能
|
||||
### 2025.06.15 添加公司公告信息搜索/查看功能
|
||||
### 2025.06.15 添加个股研报到弹出菜单
|
||||
### 2025.06.13 添加个股研报功能
|
||||
### 2025.06.12 添加龙虎榜功能,新增行业排名分类
|
||||
### 2025.05.30 优化股票分时图显示
|
||||
### 2025.05.20 修复财联社电报获取问题
|
||||
### 2025.05.16 优化资金趋势图表组件
|
||||
### 2025.05.15 重构应用加载和数据初始化逻辑,添加股票资金趋势功能,资金趋势图表增加主力当日净流入数据并优化展示效果
|
||||
### 2025.05.14 添加个股资金流向功能,排行榜增加股票行情K线图弹窗
|
||||
### 2025.05.13 添加行业排名功能
|
||||
### 2025.05.09 添加A股盘口数据解析和展示功能
|
||||
### 2025.05.07 优化分时图的展示
|
||||
### 2025.04.29 补全港股/美股基础数据,优化港股股价延迟问题,优化初始化逻辑
|
||||
### 2025.04.25 市场资讯支持AI分析和总结:让AI帮你读市场!
|
||||
### 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态,从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定!
|
||||
### 2025.04.22 优化K线图展示,支持拉伸放大,看得更舒服啦!
|
||||
### 2025.04.21 港股,美股K线数据获取优化
|
||||
### 2025.04.01 优化部分设置选项,避免重启软件
|
||||
### 2025.03.31 优化数据爬取
|
||||
### 2025.03.30 AI自动定时分析功能
|
||||
@@ -75,6 +115,12 @@ QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http
|
||||
|
||||
## 🦄 重大更新
|
||||
### BIG NEWS !!! 重大更新!!!
|
||||
- 2025.04.25 市场资讯支持AI分析和总结:让AI帮你读市场!
|
||||

|
||||
- 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态,从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定!
|
||||

|
||||

|
||||
- 
|
||||
- 2025.01.17 新增AI大模型分析股票功能
|
||||

|
||||
## 📸 功能截图
|
||||
@@ -84,7 +130,7 @@ QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http
|
||||
### 成本设置
|
||||

|
||||
### 日K
|
||||

|
||||

|
||||
### 分时
|
||||

|
||||
### 钉钉报警通知
|
||||
|
||||
64
app_common.go
Normal file
64
app_common.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/models"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/6/8 20:45
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func (a *App) LongTigerRank(date string) *[]models.LongTigerRankData {
|
||||
return data.NewMarketNewsApi().LongTiger(date)
|
||||
}
|
||||
|
||||
func (a *App) StockResearchReport(stockCode string) []any {
|
||||
return data.NewMarketNewsApi().StockResearchReport(stockCode, 7)
|
||||
}
|
||||
func (a *App) StockNotice(stockCode string) []any {
|
||||
return data.NewMarketNewsApi().StockNotice(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) IndustryResearchReport(industryCode string) []any {
|
||||
return data.NewMarketNewsApi().IndustryResearchReport(industryCode, 7)
|
||||
}
|
||||
func (a App) EMDictCode(code string) []any {
|
||||
return data.NewMarketNewsApi().EMDictCode(code, a.cache)
|
||||
}
|
||||
|
||||
func (a App) AnalyzeSentiment(text string) data.SentimentResult {
|
||||
return data.AnalyzeSentiment(text)
|
||||
}
|
||||
|
||||
func (a App) HotStock(marketType string) *[]models.HotItem {
|
||||
return data.NewMarketNewsApi().XUEQIUHotStock(100, marketType)
|
||||
}
|
||||
|
||||
func (a App) HotEvent(size int) *[]models.HotEvent {
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
return data.NewMarketNewsApi().HotEvent(size)
|
||||
}
|
||||
func (a App) HotTopic(size int) []any {
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
return data.NewMarketNewsApi().HotTopic(size)
|
||||
}
|
||||
|
||||
func (a App) InvestCalendarTimeLine(yearMonth string) []any {
|
||||
return data.NewMarketNewsApi().InvestCalendar(yearMonth)
|
||||
}
|
||||
func (a App) ClsCalendar() []any {
|
||||
return data.NewMarketNewsApi().ClsCalendar()
|
||||
}
|
||||
|
||||
func (a App) SearchStock(words string) map[string]any {
|
||||
return data.NewSearchStockApi(words).SearchStock(5000)
|
||||
}
|
||||
func (a App) GetHotStrategy() map[string]any {
|
||||
return data.NewSearchStockApi("").HotStrategy()
|
||||
}
|
||||
517
app_darwin.go
517
app_darwin.go
@@ -5,303 +5,171 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/gen2brain/beeep"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"strings"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
cacheSize := 512 * 1024
|
||||
cache := freecache.NewCache(cacheSize)
|
||||
return &App{
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// startup is called at application startup
|
||||
// startup 在应用程序启动时调用
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData)
|
||||
})
|
||||
logger.SugaredLogger.Infof("Version:%s", Version)
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
// TODO 创建系统托盘
|
||||
// 监听设置更新事件
|
||||
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
|
||||
config := &data.Settings{}
|
||||
setMap := optionalData[0].(map[string]interface{})
|
||||
|
||||
// 将 map 转换为 JSON 字节切片
|
||||
jsonData, err := json.Marshal(setMap)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
// 将 JSON 字节切片解析到结构体中
|
||||
err = json.Unmarshal(jsonData, config)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
|
||||
if config.DarkTheme {
|
||||
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
|
||||
runtime.WindowSetDarkTheme(ctx)
|
||||
} else {
|
||||
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
|
||||
runtime.WindowSetLightTheme(ctx)
|
||||
}
|
||||
runtime.WindowReloadApp(ctx)
|
||||
})
|
||||
|
||||
// 创建 macOS 托盘
|
||||
go func() {
|
||||
// 使用 Beeep 库替代 Windows 的托盘库
|
||||
err := beeep.Notify("go-stock", "应用程序已启动", "")
|
||||
if err != nil {
|
||||
log.Fatalf("系统通知失败: %v", err)
|
||||
}
|
||||
}()
|
||||
go setUpScreen(a)
|
||||
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
|
||||
}
|
||||
|
||||
func checkUpdate(a *App) {
|
||||
releaseVersion := &models.GitHubReleaseVersion{}
|
||||
_, err := resty.New().R().
|
||||
SetResult(releaseVersion).
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
|
||||
func setUpScreen(a *App) {
|
||||
screens, _ := runtime.ScreenGetAll(a.ctx)
|
||||
if len(screens) == 0 {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
|
||||
if releaseVersion.TagName != Version {
|
||||
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// domReady is called after front-end resources have been loaded
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
// Add your action here
|
||||
//定时更新数据
|
||||
go func() {
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
interval := config.RefreshInterval
|
||||
if interval <= 0 {
|
||||
interval = 1
|
||||
}
|
||||
ticker := time.NewTicker(time.Second * time.Duration(interval))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if isTradingTime(time.Now()) {
|
||||
MonitorStockPrices(a)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
telegraph := refreshTelegraphList()
|
||||
if telegraph != nil {
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
||||
go MonitorStockPrices(a)
|
||||
|
||||
//检查新版本
|
||||
go func() {
|
||||
checkUpdate(a)
|
||||
}()
|
||||
}
|
||||
|
||||
func refreshTelegraphList() *[]string {
|
||||
url := "https://www.cls.cn/telegraph"
|
||||
response, err := resty.New().R().
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(fmt.Sprintf(url))
|
||||
// OnSecondInstanceLaunch 处理第二实例启动时的通知
|
||||
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
err := beeep.Notify("go-stock", "程序已经在运行了", "")
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
logger.SugaredLogger.Error(err)
|
||||
}
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
var telegraph []string
|
||||
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
||||
//logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph = append(telegraph, selection.Text())
|
||||
})
|
||||
return &telegraph
|
||||
}
|
||||
|
||||
// isTradingDay 判断是否是交易日
|
||||
func isTradingDay(date time.Time) bool {
|
||||
weekday := date.Weekday()
|
||||
// 判断是否是周末
|
||||
if weekday == time.Saturday || weekday == time.Sunday {
|
||||
return false
|
||||
}
|
||||
// 这里可以添加具体的节假日判断逻辑
|
||||
// 例如:判断是否是春节、国庆节等
|
||||
return true
|
||||
}
|
||||
|
||||
// isTradingTime 判断是否是交易时间
|
||||
func isTradingTime(date time.Time) bool {
|
||||
if !isTradingDay(date) {
|
||||
return false
|
||||
}
|
||||
|
||||
hour, minute, _ := date.Clock()
|
||||
|
||||
// 判断是否在9:15到11:30之间
|
||||
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 判断是否在13:00到15:00之间
|
||||
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
|
||||
func MonitorStockPrices(a *App) {
|
||||
dest := &[]data.FollowedStock{}
|
||||
db.Dao.Model(&data.FollowedStock{}).Find(dest)
|
||||
total := float64(0)
|
||||
//for _, follow := range *dest {
|
||||
// stockData := getStockInfo(follow)
|
||||
// total += stockData.ProfitAmountToday
|
||||
// price, _ := convertor.ToFloat(stockData.Price)
|
||||
// if stockData.PrePrice != price {
|
||||
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
|
||||
// }
|
||||
//}
|
||||
|
||||
// 股票信息处理逻辑
|
||||
stockInfos := GetStockInfos(*dest...)
|
||||
for _, stockInfo := range *stockInfos {
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
|
||||
total += stockInfo.ProfitAmountToday
|
||||
price, _ := convertor.ToFloat(stockInfo.Price)
|
||||
|
||||
if stockInfo.PrePrice != price {
|
||||
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总收益并更新状态
|
||||
if total != 0 {
|
||||
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
||||
// systray.SetTooltip(title)
|
||||
// 使用通知替代 systray 更新 Tooltip
|
||||
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
||||
|
||||
// 发送通知显示实时数据
|
||||
err := beeep.Notify("go-stock", title, "")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("发送通知失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 触发实时利润事件
|
||||
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
|
||||
//runtime.WindowSetTitle(a.ctx, title)
|
||||
|
||||
}
|
||||
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
|
||||
stockCodes := make([]string, 0)
|
||||
for _, follow := range follows {
|
||||
stockCodes = append(stockCodes, follow.StockCode)
|
||||
}
|
||||
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
|
||||
|
||||
// onReady 在应用程序准备好时调用
|
||||
func onReady(a *App) {
|
||||
// 初始化操作
|
||||
logger.SugaredLogger.Infof("onReady")
|
||||
|
||||
// 使用 Beeep 发送通知
|
||||
err := beeep.Notify("go-stock", "应用程序已准备就绪", "")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
|
||||
return nil
|
||||
log.Fatalf("系统通知失败: %v", err)
|
||||
}
|
||||
stockInfos := make([]data.StockInfo, 0)
|
||||
for _, info := range *stockData {
|
||||
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
|
||||
return follow.StockCode == info.Code
|
||||
})
|
||||
if ok {
|
||||
addStockFollowData(v, &info)
|
||||
stockInfos = append(stockInfos, info)
|
||||
}
|
||||
}
|
||||
return &stockInfos
|
||||
}
|
||||
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
|
||||
stockCode := follow.StockCode
|
||||
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
if err != nil || len(*stockDatas) == 0 {
|
||||
return &data.StockInfo{}
|
||||
}
|
||||
stockData := (*stockDatas)[0]
|
||||
addStockFollowData(follow, &stockData)
|
||||
return &stockData
|
||||
|
||||
// 显示应用窗口
|
||||
runtime.WindowShow(a.ctx)
|
||||
|
||||
// 在 macOS 上没有系统托盘图标菜单,通常我们通过通知或其他方式提供与用户交互的界面
|
||||
}
|
||||
|
||||
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
|
||||
stockData.PrePrice = follow.Price //上次当前价格
|
||||
stockData.Sort = follow.Sort
|
||||
stockData.CostPrice = follow.CostPrice //成本价
|
||||
stockData.CostVolume = follow.Volume //成本量
|
||||
stockData.AlarmChangePercent = follow.AlarmChangePercent
|
||||
stockData.AlarmPrice = follow.AlarmPrice
|
||||
|
||||
//当前价格
|
||||
price, _ := convertor.ToFloat(stockData.Price)
|
||||
//当前价格为0 时 使用卖一价格作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.A1P)
|
||||
}
|
||||
//当前价格依然为0 时 使用买一报价作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.B1P)
|
||||
}
|
||||
|
||||
//昨日收盘价
|
||||
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
|
||||
|
||||
//当前价格依然为0 时 使用昨日收盘价为当前价格
|
||||
if price == 0 {
|
||||
price = preClosePrice
|
||||
}
|
||||
|
||||
//今日最高价
|
||||
highPrice, _ := convertor.ToFloat(stockData.High)
|
||||
if highPrice == 0 {
|
||||
highPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
|
||||
//今日最低价
|
||||
lowPrice, _ := convertor.ToFloat(stockData.Low)
|
||||
if lowPrice == 0 {
|
||||
lowPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
//开盘价
|
||||
//openPrice, _ := convertor.ToFloat(stockData.Open)
|
||||
|
||||
if price > 0 {
|
||||
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
|
||||
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if highPrice > 0 {
|
||||
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if lowPrice > 0 {
|
||||
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if follow.CostPrice > 0 && follow.Volume > 0 {
|
||||
if price > 0 {
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
|
||||
} else {
|
||||
//未开盘时当前价格为昨日收盘价
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
|
||||
if follow.Price != price && price > 0 {
|
||||
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
|
||||
"price": price,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// beforeClose is called when the application is about to quit,
|
||||
// either by clicking the window close button or calling runtime.Quit.
|
||||
// Returning true will cause the application to continue, false will continue shutdown as normal.
|
||||
// beforeClose 在应用程序关闭前调用,显示确认对话框
|
||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
defer PanicHandler()
|
||||
|
||||
// 在 macOS 上使用 MessageDialog 显示确认窗口
|
||||
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: "go-stock",
|
||||
Message: "确定关闭吗?",
|
||||
Buttons: []string{"确定"},
|
||||
Buttons: []string{"确定", "取消"},
|
||||
Icon: icon,
|
||||
CancelButton: "取消",
|
||||
})
|
||||
@@ -310,150 +178,27 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
if dialog == "取消" {
|
||||
return true // 如果选择了取消,不关闭应用
|
||||
} else {
|
||||
// 在 macOS 上应用退出时执行清理工作
|
||||
a.cron.Stop() // 停止定时任务
|
||||
return false // 如果选择了确定,继续关闭应用
|
||||
}
|
||||
}
|
||||
|
||||
func getFrameless() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
// Perform your teardown here
|
||||
// systray.Quit()
|
||||
}
|
||||
func getScreenResolution() (int, int, int, int, error) {
|
||||
//user32 := syscall.NewLazyDLL("user32.dll")
|
||||
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
//
|
||||
//width, _, _ := getSystemMetrics.Call(0)
|
||||
//height, _, _ := getSystemMetrics.Call(1)
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(stockCode string) *data.StockInfo {
|
||||
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
|
||||
follow := &data.FollowedStock{
|
||||
StockCode: stockCode,
|
||||
}
|
||||
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
|
||||
stockInfo := getStockInfo(*follow)
|
||||
return stockInfo
|
||||
}
|
||||
|
||||
func (a *App) Follow(stockCode string) string {
|
||||
return data.NewStockDataApi().Follow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) UnFollow(stockCode string) string {
|
||||
return data.NewStockDataApi().UnFollow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) GetFollowList() []data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList()
|
||||
}
|
||||
|
||||
func (a *App) GetStockList(key string) []data.StockBasic {
|
||||
return data.NewStockDataApi().GetStockList(key)
|
||||
}
|
||||
|
||||
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
|
||||
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
|
||||
}
|
||||
|
||||
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
|
||||
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
|
||||
}
|
||||
func (a *App) SetStockSort(sort int64, stockCode string) {
|
||||
data.NewStockDataApi().SetStockSort(sort, stockCode)
|
||||
}
|
||||
func (a *App) SendDingDingMessage(message string, stockCode string) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
|
||||
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
stockInfo := &data.StockInfo{}
|
||||
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
|
||||
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) NewChat(stock string) string {
|
||||
return data.NewDeepSeekOpenAi().NewChat(stock)
|
||||
}
|
||||
|
||||
func (a *App) NewChatStream(stock, stockCode string) {
|
||||
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", msg)
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
|
||||
}
|
||||
|
||||
func GenNotificationMsg(stockInfo *data.StockInfo) string {
|
||||
Price, err := convertor.ToFloat(stockInfo.Price)
|
||||
if err != nil {
|
||||
Price = 0
|
||||
}
|
||||
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
|
||||
if err != nil {
|
||||
PreClose = 0
|
||||
}
|
||||
var RF float64
|
||||
if PreClose > 0 {
|
||||
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
|
||||
}
|
||||
|
||||
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
|
||||
}
|
||||
|
||||
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
|
||||
func getMsgTypeTTL(msgType int) int {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return 60 * 5
|
||||
case 2:
|
||||
return 60 * 30
|
||||
case 3:
|
||||
return 60 * 30
|
||||
default:
|
||||
return 60 * 5
|
||||
}
|
||||
}
|
||||
|
||||
func getMsgTypeName(msgType int) string {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return "涨跌报警"
|
||||
case 2:
|
||||
return "股价报警"
|
||||
case 3:
|
||||
return "成本价报警"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
return int(1200), int(800), 0, 0, nil
|
||||
}
|
||||
|
||||
21
app_test.go
21
app_test.go
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
215
app_windows.go
Normal file
215
app_windows.go
Normal file
@@ -0,0 +1,215 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/energye/systray"
|
||||
"github.com/go-toast/toast"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
)
|
||||
|
||||
// startup is called at application startup
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData)
|
||||
})
|
||||
logger.SugaredLogger.Infof("Version:%s", Version)
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
// 创建系统托盘
|
||||
//systray.RunWithExternalLoop(func() {
|
||||
// onReady(a)
|
||||
//}, func() {
|
||||
// onExit(a)
|
||||
//})
|
||||
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
|
||||
config := &data.Settings{}
|
||||
setMap := optionalData[0].(map[string]interface{})
|
||||
|
||||
// 将 map 转换为 JSON 字节切片
|
||||
jsonData, err := json.Marshal(setMap)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
// 将 JSON 字节切片解析到结构体中
|
||||
err = json.Unmarshal(jsonData, config)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
|
||||
if config.DarkTheme {
|
||||
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
|
||||
runtime.WindowSetDarkTheme(ctx)
|
||||
} else {
|
||||
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
|
||||
runtime.WindowSetLightTheme(ctx)
|
||||
}
|
||||
runtime.WindowReloadApp(ctx)
|
||||
|
||||
})
|
||||
go systray.Run(func() {
|
||||
onReady(a)
|
||||
}, func() {
|
||||
onExit(a)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
|
||||
}
|
||||
|
||||
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
notification := toast.Notification{
|
||||
AppID: "go-stock",
|
||||
Title: "go-stock",
|
||||
Message: "程序已经在运行了",
|
||||
Icon: "",
|
||||
Duration: "short",
|
||||
Audio: toast.Default,
|
||||
}
|
||||
err := notification.Push()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
}
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
|
||||
func MonitorStockPrices(a *App) {
|
||||
dest := &[]data.FollowedStock{}
|
||||
db.Dao.Model(&data.FollowedStock{}).Find(dest)
|
||||
total := float64(0)
|
||||
//for _, follow := range *dest {
|
||||
// stockData := getStockInfo(follow)
|
||||
// total += stockData.ProfitAmountToday
|
||||
// price, _ := convertor.ToFloat(stockData.Price)
|
||||
// if stockData.PrePrice != price {
|
||||
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
|
||||
// }
|
||||
//}
|
||||
|
||||
stockInfos := GetStockInfos(*dest...)
|
||||
for _, stockInfo := range *stockInfos {
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
|
||||
total += stockInfo.ProfitAmountToday
|
||||
price, _ := convertor.ToFloat(stockInfo.Price)
|
||||
|
||||
if stockInfo.PrePrice != price {
|
||||
//logger.SugaredLogger.Infof("-----------sz------------股票代码: %s, 股票名称: %s, 股票价格: %s,盘前盘后:%s", stockInfo.Code, stockInfo.Name, stockInfo.Price, stockInfo.BA)
|
||||
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
|
||||
}
|
||||
|
||||
}
|
||||
if total != 0 {
|
||||
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
||||
systray.SetTooltip(title)
|
||||
}
|
||||
|
||||
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
|
||||
//runtime.WindowSetTitle(a.ctx, title)
|
||||
|
||||
}
|
||||
|
||||
func onReady(a *App) {
|
||||
|
||||
// 初始化操作
|
||||
logger.SugaredLogger.Infof("systray onReady")
|
||||
systray.SetIcon(icon2)
|
||||
systray.SetTitle("go-stock")
|
||||
systray.SetTooltip("go-stock 股票行情实时获取")
|
||||
// 创建菜单项
|
||||
show := systray.AddMenuItem("显示", "显示应用程序")
|
||||
show.Click(func() {
|
||||
//logger.SugaredLogger.Infof("显示应用程序")
|
||||
runtime.WindowShow(a.ctx)
|
||||
})
|
||||
hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
|
||||
hide.Click(func() {
|
||||
//logger.SugaredLogger.Infof("隐藏应用程序")
|
||||
runtime.WindowHide(a.ctx)
|
||||
})
|
||||
systray.AddSeparator()
|
||||
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
|
||||
mQuitOrig.Click(func() {
|
||||
//logger.SugaredLogger.Infof("退出应用程序")
|
||||
runtime.Quit(a.ctx)
|
||||
})
|
||||
systray.SetOnRClick(func(menu systray.IMenu) {
|
||||
menu.ShowMenu()
|
||||
//logger.SugaredLogger.Infof("SetOnRClick")
|
||||
})
|
||||
systray.SetOnClick(func(menu systray.IMenu) {
|
||||
//logger.SugaredLogger.Infof("SetOnClick")
|
||||
menu.ShowMenu()
|
||||
})
|
||||
systray.SetOnDClick(func(menu systray.IMenu) {
|
||||
menu.ShowMenu()
|
||||
//logger.SugaredLogger.Infof("SetOnDClick")
|
||||
})
|
||||
}
|
||||
|
||||
// beforeClose is called when the application is about to quit,
|
||||
// either by clicking the window close button or calling runtime.Quit.
|
||||
// Returning true will cause the application to continue, false will continue shutdown as normal.
|
||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
defer PanicHandler()
|
||||
|
||||
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: "go-stock",
|
||||
Message: "确定关闭吗?",
|
||||
Buttons: []string{"确定"},
|
||||
Icon: icon,
|
||||
CancelButton: "取消",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
|
||||
return false
|
||||
}
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
} else {
|
||||
systray.Quit()
|
||||
a.cron.Stop()
|
||||
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
|
||||
}
|
||||
@@ -344,7 +344,7 @@ func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
|
||||
//logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent)
|
||||
err := json.Unmarshal([]byte(htmlContent), &fundNetUnitValue)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
//logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
fund := &FollowedFund{
|
||||
|
||||
859
backend/data/market_news_api.go
Normal file
859
backend/data/market_news_api.go
Normal file
@@ -0,0 +1,859 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/samber/lo"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/4/23 14:54
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type MarketNewsApi struct {
|
||||
}
|
||||
|
||||
func NewMarketNewsApi() *MarketNewsApi {
|
||||
return &MarketNewsApi{}
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
|
||||
url := "https://www.cls.cn/telegraph"
|
||||
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(fmt.Sprintf(url))
|
||||
var telegraphs []models.Telegraph
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, _ := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
|
||||
document.Find(".telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
||||
//logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph := models.Telegraph{Source: "财联社电报"}
|
||||
spans := selection.Find("div.telegraph-content-box span")
|
||||
if spans.Length() == 2 {
|
||||
telegraph.Time = spans.First().Text()
|
||||
telegraph.Content = spans.Last().Text()
|
||||
if spans.Last().HasClass("c-de0422") {
|
||||
telegraph.IsRed = true
|
||||
}
|
||||
}
|
||||
|
||||
labels := selection.Find("div a.label-item")
|
||||
labels.Each(func(i int, selection *goquery.Selection) {
|
||||
if selection.HasClass("link-label-item") {
|
||||
telegraph.Url = selection.AttrOr("href", "")
|
||||
} else {
|
||||
tag := &models.Tags{
|
||||
Name: selection.Text(),
|
||||
Type: "subject",
|
||||
}
|
||||
db.Dao.Model(tag).Where("name=? and type=?", selection.Text(), "subject").FirstOrCreate(&tag)
|
||||
telegraph.SubjectTags = append(telegraph.SubjectTags, selection.Text())
|
||||
}
|
||||
})
|
||||
stocks := selection.Find("div.telegraph-stock-plate-box a")
|
||||
stocks.Each(func(i int, selection *goquery.Selection) {
|
||||
telegraph.StocksTags = append(telegraph.StocksTags, selection.Text())
|
||||
})
|
||||
|
||||
//telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
|
||||
if telegraph.Content != "" {
|
||||
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
|
||||
cnt := int64(0)
|
||||
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
|
||||
if cnt == 0 {
|
||||
db.Dao.Create(&telegraph)
|
||||
telegraphs = append(telegraphs, telegraph)
|
||||
for _, tag := range telegraph.SubjectTags {
|
||||
tagInfo := &models.Tags{}
|
||||
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "subject").First(&tagInfo)
|
||||
if tagInfo.ID > 0 {
|
||||
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
|
||||
TelegraphId: telegraph.ID,
|
||||
TagId: tagInfo.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
return &telegraphs
|
||||
}
|
||||
func (m MarketNewsApi) GetNewsList(source string, limit int) *[]*models.Telegraph {
|
||||
news := &[]*models.Telegraph{}
|
||||
if source != "" {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(limit).Find(news)
|
||||
} else {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(limit).Find(news)
|
||||
}
|
||||
for _, item := range *news {
|
||||
tags := &[]models.Tags{}
|
||||
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
|
||||
return item.TagId
|
||||
})).Find(&tags)
|
||||
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
|
||||
return item.Name
|
||||
})
|
||||
item.SubjectTags = tagNames
|
||||
logger.SugaredLogger.Infof("tagNames %v ,SubjectTags:%s", tagNames, item.SubjectTags)
|
||||
}
|
||||
return news
|
||||
}
|
||||
func (m MarketNewsApi) GetTelegraphList(source string) *[]*models.Telegraph {
|
||||
news := &[]*models.Telegraph{}
|
||||
if source != "" {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(20).Find(news)
|
||||
} else {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(20).Find(news)
|
||||
}
|
||||
for _, item := range *news {
|
||||
tags := &[]models.Tags{}
|
||||
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
|
||||
return item.TagId
|
||||
})).Find(&tags)
|
||||
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
|
||||
return item.Name
|
||||
})
|
||||
item.SubjectTags = tagNames
|
||||
logger.SugaredLogger.Infof("tagNames %v ,SubjectTags:%s", tagNames, item.SubjectTags)
|
||||
}
|
||||
return news
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
|
||||
news := &[]models.Telegraph{}
|
||||
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get("https://zhibo.sina.com.cn/api/zhibo/feed?callback=callback&page=1&page_size=20&zhibo_id=152&tag_id=0&dire=f&dpc=1&pagesize=20&id=4161089&type=0&_=" + strconv.FormatInt(time.Now().Unix(), 10))
|
||||
js := string(response.Body())
|
||||
js = strutil.ReplaceWithMap(js, map[string]string{
|
||||
"try{callback(": "var data=",
|
||||
");}catch(e){};": ";",
|
||||
})
|
||||
//logger.SugaredLogger.Info(js)
|
||||
vm := otto.New()
|
||||
_, err := vm.Run(js)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
}
|
||||
vm.Run("var result = data.result;")
|
||||
//vm.Run("var resultStr =JSON.stringify(data);")
|
||||
vm.Run("var resultData = result.data;")
|
||||
vm.Run("var feed = resultData.feed;")
|
||||
vm.Run("var feedStr = JSON.stringify(feed);")
|
||||
|
||||
value, _ := vm.Get("feedStr")
|
||||
//resultStr, _ := vm.Get("resultStr")
|
||||
|
||||
//logger.SugaredLogger.Info(resultStr)
|
||||
feed := make(map[string]any)
|
||||
err = json.Unmarshal([]byte(value.String()), &feed)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
|
||||
}
|
||||
var telegraphs []models.Telegraph
|
||||
|
||||
if feed["list"] != nil {
|
||||
for _, item := range feed["list"].([]any) {
|
||||
telegraph := models.Telegraph{Source: "新浪财经"}
|
||||
data := item.(map[string]any)
|
||||
//logger.SugaredLogger.Infof("%s:%s", data["create_time"], data["rich_text"])
|
||||
telegraph.Content = data["rich_text"].(string)
|
||||
telegraph.Time = strings.Split(data["create_time"].(string), " ")[1]
|
||||
tags := data["tag"].([]any)
|
||||
telegraph.SubjectTags = lo.Map(tags, func(tagItem any, index int) string {
|
||||
name := tagItem.(map[string]any)["name"].(string)
|
||||
tag := &models.Tags{
|
||||
Name: name,
|
||||
Type: "sina_subject",
|
||||
}
|
||||
db.Dao.Model(tag).Where("name=? and type=?", name, "sina_subject").FirstOrCreate(&tag)
|
||||
return name
|
||||
})
|
||||
if _, ok := lo.Find(telegraph.SubjectTags, func(item string) bool { return item == "焦点" }); ok {
|
||||
telegraph.IsRed = true
|
||||
}
|
||||
logger.SugaredLogger.Infof("telegraph.SubjectTags:%v %s", telegraph.SubjectTags, telegraph.Content)
|
||||
|
||||
if telegraph.Content != "" {
|
||||
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
|
||||
cnt := int64(0)
|
||||
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
|
||||
if cnt == 0 {
|
||||
db.Dao.Create(&telegraph)
|
||||
telegraphs = append(telegraphs, telegraph)
|
||||
for _, tag := range telegraph.SubjectTags {
|
||||
tagInfo := &models.Tags{}
|
||||
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "sina_subject").First(&tagInfo)
|
||||
if tagInfo.ID > 0 {
|
||||
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
|
||||
TelegraphId: telegraph.ID,
|
||||
TagId: tagInfo.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &telegraphs
|
||||
}
|
||||
|
||||
return news
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GlobalStockIndexes(crawlTimeOut uint) map[string]any {
|
||||
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("Referer", "https://stockapp.finance.qq.com/mstats").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/rank/indexRankDetail2")
|
||||
js := string(response.Body())
|
||||
res := make(map[string]any)
|
||||
json.Unmarshal([]byte(js), &res)
|
||||
return res["data"].(map[string]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetIndustryRank(sort string, cnt int) map[string]any {
|
||||
|
||||
url := fmt.Sprintf("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/mktHs/rank?l=%d&p=1&t=01/averatio&ordertype=&o=%s", cnt, sort)
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Referer", "https://stockapp.finance.qq.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(url)
|
||||
js := string(response.Body())
|
||||
res := make(map[string]any)
|
||||
json.Unmarshal([]byte(js), &res)
|
||||
return res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetIndustryMoneyRankSina(fenlei, sort string) []map[string]any {
|
||||
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_bk?page=1&num=20&sort=%s&asc=0&fenlei=%s", sort, fenlei)
|
||||
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(url)
|
||||
js := string(response.Body())
|
||||
res := &[]map[string]any{}
|
||||
err := json.Unmarshal([]byte(js), &res)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return *res
|
||||
}
|
||||
return *res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetMoneyRankSina(sort string) []map[string]any {
|
||||
if sort == "" {
|
||||
sort = "netamount"
|
||||
}
|
||||
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_ssggzj?page=1&num=20&sort=%s&asc=0&bankuai=&shichang=", sort)
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(url)
|
||||
js := string(response.Body())
|
||||
res := &[]map[string]any{}
|
||||
err := json.Unmarshal([]byte(js), &res)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return *res
|
||||
}
|
||||
return *res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]any {
|
||||
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_qsfx_zjlrqs?page=1&num=%d&sort=opendate&asc=0&daima=%s", days, stockCode)
|
||||
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
|
||||
js := string(response.Body())
|
||||
res := &[]map[string]any{}
|
||||
err := json.Unmarshal([]byte(js), &res)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return *res
|
||||
}
|
||||
return *res
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) TopStocksRankingList(date string) {
|
||||
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/q/go.php/vInvestConsult/kind/lhb/index.phtml?tradedate=%s", date)
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
|
||||
|
||||
html, _ := convertor.GbkToUtf8(response.Body())
|
||||
//logger.SugaredLogger.Infof("html:%s", html)
|
||||
document, err := goquery.NewDocumentFromReader(bytes.NewReader(html))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
document.Find("table.list_table").Each(func(i int, s *goquery.Selection) {
|
||||
title := strutil.Trim(s.Find("tr:first-child").First().Text())
|
||||
logger.SugaredLogger.Infof("title:%s", title)
|
||||
s.Find("tr:not(:first-child)").Each(func(i int, s *goquery.Selection) {
|
||||
logger.SugaredLogger.Infof("s:%s", strutil.RemoveNonPrintable(s.Text()))
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) LongTiger(date string) *[]models.LongTigerRankData {
|
||||
ranks := &[]models.LongTigerRankData{}
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get"
|
||||
logger.SugaredLogger.Infof("url:%s", url)
|
||||
params := make(map[string]string)
|
||||
params["callback"] = "callback"
|
||||
params["sortColumns"] = "TURNOVERRATE,TRADE_DATE,SECURITY_CODE"
|
||||
params["sortTypes"] = "-1,-1,1"
|
||||
params["pageSize"] = "500"
|
||||
params["pageNumber"] = "1"
|
||||
params["reportName"] = "RPT_DAILYBILLBOARD_DETAILSNEW"
|
||||
params["columns"] = "SECURITY_CODE,SECUCODE,SECURITY_NAME_ABBR,TRADE_DATE,EXPLAIN,CLOSE_PRICE,CHANGE_RATE,BILLBOARD_NET_AMT,BILLBOARD_BUY_AMT,BILLBOARD_SELL_AMT,BILLBOARD_DEAL_AMT,ACCUM_AMOUNT,DEAL_NET_RATIO,DEAL_AMOUNT_RATIO,TURNOVERRATE,FREE_MARKET_CAP,EXPLANATION,D1_CLOSE_ADJCHRATE,D2_CLOSE_ADJCHRATE,D5_CLOSE_ADJCHRATE,D10_CLOSE_ADJCHRATE,SECURITY_TYPE_CODE"
|
||||
params["source"] = "WEB"
|
||||
params["client"] = "WEB"
|
||||
params["filter"] = fmt.Sprintf("(TRADE_DATE<='%s')(TRADE_DATE>='%s')", date, date)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/stock/tradedetail.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetQueryParams(params).
|
||||
Get(url)
|
||||
if err != nil {
|
||||
return ranks
|
||||
}
|
||||
js := string(resp.Body())
|
||||
logger.SugaredLogger.Infof("resp:%s", js)
|
||||
|
||||
js = strutil.ReplaceWithMap(js, map[string]string{
|
||||
"callback(": "var data=",
|
||||
");": ";",
|
||||
})
|
||||
//logger.SugaredLogger.Info(js)
|
||||
vm := otto.New()
|
||||
_, err = vm.Run(js)
|
||||
_, err = vm.Run("var data = JSON.stringify(data);")
|
||||
value, err := vm.Get("data")
|
||||
logger.SugaredLogger.Infof("resp-json:%s", value.String())
|
||||
data := gjson.Get(value.String(), "result.data")
|
||||
logger.SugaredLogger.Infof("resp:%v", data)
|
||||
err = json.Unmarshal([]byte(data.String()), ranks)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return ranks
|
||||
}
|
||||
for _, rankData := range *ranks {
|
||||
temp := &models.LongTigerRankData{}
|
||||
db.Dao.Model(temp).Where(&models.LongTigerRankData{
|
||||
TRADEDATE: rankData.TRADEDATE,
|
||||
SECUCODE: rankData.SECUCODE,
|
||||
}).First(temp)
|
||||
if temp.SECURITYTYPECODE == "" {
|
||||
db.Dao.Model(temp).Create(&rankData)
|
||||
}
|
||||
}
|
||||
return ranks
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) IndustryResearchReport(industryCode string, days int) []any {
|
||||
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
|
||||
endDate := time.Now().Format("2006-01-02")
|
||||
if strutil.Trim(industryCode) != "" {
|
||||
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("IndustryResearchReport-name:%s", industryCode)
|
||||
params := map[string]string{
|
||||
"industry": "*",
|
||||
"industryCode": industryCode,
|
||||
"beginTime": beginDate,
|
||||
"endTime": endDate,
|
||||
"pageNo": "1",
|
||||
"pageSize": "50",
|
||||
"p": "1",
|
||||
"pageNum": "1",
|
||||
"pageNumber": "1",
|
||||
"qType": "1",
|
||||
}
|
||||
|
||||
url := "https://reportapi.eastmoney.com/report/list"
|
||||
|
||||
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "reportapi.eastmoney.com").
|
||||
SetHeader("Origin", "https://data.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetQueryParams(params).Get(url)
|
||||
respMap := map[string]any{}
|
||||
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
func (m MarketNewsApi) StockResearchReport(stockCode string, days int) []any {
|
||||
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
|
||||
endDate := time.Now().Format("2006-01-02")
|
||||
if strutil.ContainsAny(stockCode, []string{"."}) {
|
||||
stockCode = strings.Split(stockCode, ".")[0]
|
||||
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
|
||||
} else {
|
||||
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
|
||||
"sh": "",
|
||||
"sz": "",
|
||||
"gb_": "",
|
||||
"us": "",
|
||||
"us_": "",
|
||||
})
|
||||
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("StockResearchReport-stockCode:%s", stockCode)
|
||||
|
||||
type Req struct {
|
||||
BeginTime string `json:"beginTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
IndustryCode string `json:"industryCode"`
|
||||
RatingChange string `json:"ratingChange"`
|
||||
Rating string `json:"rating"`
|
||||
OrgCode interface{} `json:"orgCode"`
|
||||
Code string `json:"code"`
|
||||
Rcode string `json:"rcode"`
|
||||
PageSize int `json:"pageSize"`
|
||||
PageNo int `json:"pageNo"`
|
||||
P int `json:"p"`
|
||||
PageNum int `json:"pageNum"`
|
||||
PageNumber int `json:"pageNumber"`
|
||||
}
|
||||
|
||||
url := "https://reportapi.eastmoney.com/report/list2"
|
||||
|
||||
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "reportapi.eastmoney.com").
|
||||
SetHeader("Origin", "https://data.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(&Req{
|
||||
Code: stockCode,
|
||||
IndustryCode: "*",
|
||||
BeginTime: beginDate,
|
||||
EndTime: endDate,
|
||||
PageNo: 1,
|
||||
PageSize: 50,
|
||||
P: 1,
|
||||
PageNum: 1,
|
||||
PageNumber: 1,
|
||||
}).Post(url)
|
||||
respMap := map[string]any{}
|
||||
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) StockNotice(stock_list string) []any {
|
||||
var stockCodes []string
|
||||
for _, stockCode := range strings.Split(stock_list, ",") {
|
||||
if strutil.ContainsAny(stockCode, []string{"."}) {
|
||||
stockCode = strings.Split(stockCode, ".")[0]
|
||||
stockCodes = append(stockCodes, stockCode)
|
||||
} else {
|
||||
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
|
||||
"sh": "",
|
||||
"sz": "",
|
||||
"gb_": "",
|
||||
"us": "",
|
||||
"us_": "",
|
||||
})
|
||||
stockCodes = append(stockCodes, stockCode)
|
||||
}
|
||||
}
|
||||
|
||||
url := "https://np-anotice-stock.eastmoney.com/api/security/ann?page_size=50&page_index=1&ann_type=SHA%2CCYB%2CSZA%2CBJA%2CINV&client_source=web&f_node=0&stock_list=" + strings.Join(stockCodes, ",")
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "np-anotice-stock.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/notices/hsa/5.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
respMap := map[string]any{}
|
||||
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return (respMap["data"].(map[string]any))["list"].([]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
|
||||
respMap := map[string]any{}
|
||||
|
||||
d, _ := cache.Get([]byte(code))
|
||||
if d != nil {
|
||||
json.Unmarshal(d, &respMap)
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
|
||||
url := "https://reportapi.eastmoney.com/report/bk"
|
||||
|
||||
params := map[string]string{
|
||||
"bkCode": code,
|
||||
}
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "reportapi.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").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetQueryParams(params).Get(url)
|
||||
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
cache.Set([]byte(code), resp.Body(), 60*60*24)
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
|
||||
TVNews := &[]models.TVNews{}
|
||||
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=provider:panews,reuters&client=screener&streaming=false"
|
||||
resp, err := resty.New().SetProxy("http://127.0.0.1:10809").SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "news-mediator.tradingview.com").
|
||||
SetHeader("Origin", "https://cn.tradingview.com").
|
||||
SetHeader("Referer", "https://cn.tradingview.com/").
|
||||
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("TradingViewNews err:%s", err.Error())
|
||||
return TVNews
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
if err != nil {
|
||||
return TVNews
|
||||
}
|
||||
items, err := json.Marshal(respMap["items"])
|
||||
if err != nil {
|
||||
return TVNews
|
||||
}
|
||||
json.Unmarshal(items, TVNews)
|
||||
return TVNews
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.HotItem {
|
||||
request := resty.New().SetTimeout(time.Duration(30) * time.Second).R()
|
||||
_, err := request.
|
||||
SetHeader("Host", "xueqiu.com").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get("https://xueqiu.com/hq#hot")
|
||||
|
||||
//cookies := resp.Header().Get("Set-Cookie")
|
||||
//logger.SugaredLogger.Infof("cookies:%s", cookies)
|
||||
|
||||
url := fmt.Sprintf("https://stock.xueqiu.com/v5/stock/hot_stock/list.json?page=1&size=%d&_type=%s&type=%s", size, marketType, marketType)
|
||||
res := &models.XUEQIUHot{}
|
||||
_, err = request.
|
||||
SetHeader("Host", "stock.xueqiu.com").
|
||||
SetHeader("Origin", "https://xueqiu.com").
|
||||
SetHeader("Referer", "https://xueqiu.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
//SetHeader("Cookie", "cookiesu=871730774144180; device_id=ee75cebba8a35005c9e7baf7b7dead59; s=ch12b12pfi; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746247619; xq_a_token=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xqat=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xq_r_token=450d1db0db9659a6af7cc9297bfa4fccf1776fae; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MzgzODAwNiwiY3RtIjoxNzUxMjUxMzc2MDY3LCJjaWQiOiJkOWQwbjRBWnVwIn0.TjEtQ5WEN4ajnVjVnY3J-Qq9LjL-F0eat9Cefv_tLJLqsPhzD2y8Lc1CeIu0Ceqhlad7O_yW1tR9nb2dIjDpyOPzWKxvwSOKXLm8XMoz4LMgE2pysBCH4TsetzHsEOhBsY467q-JX3WoFuqo-dqv1FfLSondZCspjEMFdgPFt2V-2iXJY05YUwcBVUvL74mT9ZjNq0KaDeRBJk_il6UR8yibG7RMbe9xWYz5dSO_wJwWuxvnZ8u9EXC2m-TV7-QHVxFHR_5e8Fodrzg0yIcLU4wBTSoIIQDUKqngajX2W-nUAdo6fr78NNDmoswFVH7T7XMuQciMAqj9MpMCVW3Sog; u=871730774144180; ssxmod_itna=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZ9xGN4oYxiNDAPq0iDC+Wjxs9Orw5KQb9iqP4MAn0TbNsbtU22eqbCe=S3vTv6xoDHxY=DU1GzeieDx=PD5xDTDWeDGDD3DmnsDi5YD0KDjBYpH+omDYPDEBYDaxDbDimwY4GCrDDCtc5Dw6bmzDDzznL5WWAPzWffZg3YcFgxf8GwD7y3Dla4rMhw23=cz0Efdk0A5hYDXotDvhoY1/H6neEvOt3o=Q0ruT+5RuxoRhDxCmh5tGP32xBD5G0xS2xcb4quDK0Dy2ZmY/DDWM0qmEeSEDeOCIq1fw1misCY=WAzoOtMwDzGdUjpRk5Z0xQBDI2IMw4H7qNiNBLxWiDD; ssxmod_itna2=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZYxD3boBmiEPtDFOEPAeFmDDsuGSxf46oGKwGHd8wtUjFe+oV1lxUzutkGly=nCyCjq=UTHxMxFCr1DsFiKPuEpPVO7GrOyk5Aymnc0+11AFND7v16PvwrFQH4I72=3O1OpK7rGw+poWNCxjj=Ka5QDFWAvEzrDFQcIH=GpKpS90FAyIzGcTyck+yhQKaojn96dRqeIh=HkaFrlGnKwzO+a49=F7/c/MejoR3QM20K9IIOymrMN2bsk2TRdKFiaf4O0ut2MauiOER=iQNW2WVgDrkKzD=57r577wEx2hwkqhf8T8BDvkHZRDirC0bNK4O=G3TSkd3wYwq8bst0t9qF/e3M87NYtU2IWYWzqd=BqEfdqGq0R8wxmqLzpeGeuwSTq1OAiB87gDrozjnGkwDKRdrLz8uDjQKVlGhWk8Wd/rXQjx4pG=BNqpW/6TS1wpfxzGf5CrUhtt0j0wC5AUFo2GbX+QXPzD2guxKXrx8lZUQlwWIHyEUz+OLh0eWUkfHfM0YWXlgOejnuUa06rW9y5maDPipGms751hxKcqLq62pQty4iX3QDF6SRQd3tfEBf3CH7r2xe2qq0qdOI5Ge=GezD/Us5Z0xQBwVAZ2N/XvD0HDD").
|
||||
SetResult(res).
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
|
||||
return &[]models.HotItem{}
|
||||
}
|
||||
//logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
|
||||
return &res.Data.Items
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) HotEvent(size int) *[]models.HotEvent {
|
||||
events := &[]models.HotEvent{}
|
||||
url := fmt.Sprintf("https://xueqiu.com/hot_event/list.json?count=%d", size)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "xueqiu.com").
|
||||
SetHeader("Origin", "https://xueqiu.com").
|
||||
SetHeader("Referer", "https://xueqiu.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Cookie", "cookiesu=2617378771242871; s=c2121pp1u71; device_id=237a58584ec58d8e4d4e1040700a644f1; Hm_lvt_1db88642e346389874251b5a1eded6e3=1744100219,1744599115; xq_a_token=b7259d09435458cc3f1a963479abb270a1a016ce; xqat=b7259d09435458cc3f1a963479abb270a1a016ce; xq_r_token=28108bfa1d92ac8a46bbb57722633746218621a3; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MjU0MTk4OCwiY3RtIjoxNzUwMjMwNjA2NzI0LCJjaWQiOiJkOWQwbjRBWnVwIn0.kU_fz0luJoE7nr-K4UrNUi5-mAG-vMdXtuC4mUKIppILId4UpF70LB70yunxGiNSw6tPFR3-hyLvztKAHtekCUTm3XjUl5b3tEDP-ZUVqHnWXO_5hoeMI8h-Cfx6ZGlIr5x3icvTPkT0OV5CD5A33-ZDTKhKPf-DhJ_-m7CG5GbX4MseOBeMXuLUQUiYHPKhX1QUc0GTGrCzi8Mki0z49D0LVqCSgbsx3UGfowOOyx85_cXb4OAFvIjwbs2p0o_h-ibIT0ngVkkAyEDetVvlcZ_bkardhseCB7k9BEMgH2z8ihgkVxyy3P0degLmDUruhmqn5uZOCi1pVBDvCv9lBg; u=261737877124287; ssxmod_itna=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqEeDBubrDSxD67DK4GTm+ogiw1o3B=xedQHDgBtN=7/i1K53N+rOjquLMU=kbqYxB3DExGkqj0tPi4DxaPD5xDTDWeDGDD3DnnsDQKDRx0kL0oDIxD1D0bmHUEvh38mDYePLmOmDYPYx94Y8KoDeEgsD7HUl/vIGGEAqjLPFegXLD0HolCqr4DCid1qDm+ECfkjDn9sD0KP8fn+CRoDv=tYr4ibx+o=W+8vstf9mjGe3cXseWdBmoFrmf4DA3bFAxnAxD7vYxADaDoerDGHPoxHF+PKGPtDKmiqQGeB5qbi4eg4KDHKDe3DeG0qeEP9xVUoHDDWMYYM0ICr4FBimBDM7D0x4QOECmhul5QCN/m5/74lGm=7x9Wp7A+i7xQ7wlMD4D; ssxmod_itna2=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqoDirSDhPmGD24GajjDuGE3m7or4DlxOSGewHl6iaus2Q62SRX5CFjCds6ltF9xy6iaUuB262UkhRA8UXST=4/b+y3kGKzlGE8T29FA008ljy9jXXC7f7m7QsK667mlUooWrofk=qGZjxtcUrN1NtuAnne1hj+rQP5UnlFkxf+o7VjmatH7u7bCDlbTt3cz6CH9Fl4vye16W/ellc8I3Q37W7ZwiLGD/zPpZcnd2nsqqo/+zRbKAmz4plzwaDqGUe7f9E+P0IFRKqpRv+buQFHBSpcbwND7Q+9XWmnjI2UwKd98jIS3gPXwxvbx4OuiyH8gZ+OEt7DgE/AY/9W4VxDZrlFWyWnC4y4/I0IpAfaGKpbPmauKbkqawqv93vSf+9HamGe0Dt2PNgT3yiEB4vQP2/DdVpcGBOjFujWoHP32OshLPYI20LRCKddwEGkKqPzPwKPc3X5zuB=w2fUdtwKsAW5kQtsl8clNwjC5uDYrxR0h9xaj0xmD+YuI3GPT7xYTalRImPj2wL2=+91a304xa4bTWtP=dLGARhb/efRi0uktaz8i8C04G0x/ZWUzqRza8GGU=FfRfvb4GZM/q2rVsl0nLvRjGeAKgocLouyXs/uwZu3YxbAx30qCbjG1A533zAxIeIgD=0VAc3ixD").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("HotEvent err:%s", err.Error())
|
||||
return events
|
||||
}
|
||||
//logger.SugaredLogger.Infof("HotEvent:%s", resp.Body())
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
items, err := json.Marshal(respMap["list"])
|
||||
if err != nil {
|
||||
return events
|
||||
}
|
||||
json.Unmarshal(items, events)
|
||||
return events
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) HotTopic(size int) []any {
|
||||
url := "https://gubatopic.eastmoney.com/interface/GetData.aspx?path=newtopic/api/Topic/HomePageListRead"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "gubatopic.eastmoney.com").
|
||||
SetHeader("Origin", "https://gubatopic.eastmoney.com").
|
||||
SetHeader("Referer", "https://gubatopic.eastmoney.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetFormData(map[string]string{
|
||||
"param": fmt.Sprintf("ps=%d&p=1&type=0", size),
|
||||
"path": "newtopic/api/Topic/HomePageListRead",
|
||||
"env": "2",
|
||||
}).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("HotTopic err:%s", err.Error())
|
||||
return []any{}
|
||||
}
|
||||
//logger.SugaredLogger.Infof("HotTopic:%s", resp.Body())
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
return respMap["re"].([]any)
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) InvestCalendar(yearMonth string) []any {
|
||||
if yearMonth == "" {
|
||||
yearMonth = time.Now().Format("2006-01")
|
||||
}
|
||||
|
||||
url := "https://app.jiuyangongshe.com/jystock-app/api/v1/timeline/list"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "app.jiuyangongshe.com").
|
||||
SetHeader("Origin", "https://www.jiuyangongshe.com").
|
||||
SetHeader("Referer", "https://www.jiuyangongshe.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("token", "1cc6380a05c652b922b3d85124c85473").
|
||||
SetHeader("platform", "3").
|
||||
SetHeader("Cookie", "SESSION=NDZkNDU2ODYtODEwYi00ZGZkLWEyY2ItNjgxYzY4ZWMzZDEy").
|
||||
SetHeader("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)).
|
||||
SetBody(map[string]string{
|
||||
"date": yearMonth,
|
||||
"grade": "0",
|
||||
}).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("InvestCalendar err:%s", err.Error())
|
||||
return []any{}
|
||||
}
|
||||
//logger.SugaredLogger.Infof("InvestCalendar:%s", resp.Body())
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
return respMap["data"].([]any)
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) ClsCalendar() []any {
|
||||
url := "https://www.cls.cn/api/calendar/web/list?app=CailianpressWeb&flag=0&os=web&sv=8.4.6&type=0&sign=4b839750dc2f6b803d1c8ca00d2b40be"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "www.cls.cn").
|
||||
SetHeader("Origin", "https://www.cls.cn").
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
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("ClsCalendar err:%s", err.Error())
|
||||
return []any{}
|
||||
}
|
||||
respMap := map[string]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)
|
||||
}
|
||||
206
backend/data/market_news_api_test.go
Normal file
206
backend/data/market_news_api_test.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/util"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/4/23 17:58
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestGetSinaNews(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
NewMarketNewsApi().GetSinaNews(30)
|
||||
//NewMarketNewsApi().GetNewTelegraph(30)
|
||||
|
||||
}
|
||||
|
||||
func TestGlobalStockIndexes(t *testing.T) {
|
||||
resp := NewMarketNewsApi().GlobalStockIndexes(30)
|
||||
bytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Debugf("resp: %+v", string(bytes))
|
||||
}
|
||||
|
||||
func TestGetIndustryRank(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetIndustryRank("0", 10)
|
||||
for s, a := range res["data"].([]any) {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", s, a)
|
||||
|
||||
}
|
||||
}
|
||||
func TestGetIndustryMoneyRankSina(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetIndustryMoneyRankSina("0", "netamount")
|
||||
for i, re := range res {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
|
||||
|
||||
}
|
||||
}
|
||||
func TestGetMoneyRankSina(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetMoneyRankSina("r3_net")
|
||||
for i, re := range res {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStockMoneyTrendByDay(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetStockMoneyTrendByDay("sh600438", 360)
|
||||
for i, re := range res {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
|
||||
}
|
||||
}
|
||||
func TestTopStocksRankingList(t *testing.T) {
|
||||
NewMarketNewsApi().TopStocksRankingList("2025-05-19")
|
||||
}
|
||||
|
||||
func TestLongTiger(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
NewMarketNewsApi().LongTiger("2025-06-08")
|
||||
}
|
||||
|
||||
func TestStockResearchReport(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().StockResearchReport("600584.sh", 7)
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndustryResearchReport(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStockNotice(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().StockNotice("600584,600900")
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEMDictCode(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().EMDictCode("016", freecache.NewCache(100))
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTradingViewNews(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().TradingViewNews()
|
||||
for _, a := range *resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXUEQIUHotStock(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().XUEQIUHotStock(50, "10")
|
||||
for _, a := range *res {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHotEvent(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().HotEvent(50)
|
||||
for _, a := range *res {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHotTopic(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().HotTopic(10)
|
||||
for _, a := range res {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInvestCalendar(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().InvestCalendar("2025-06")
|
||||
for _, a := range res {
|
||||
bytes, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
date := gjson.Get(string(bytes), "date")
|
||||
list := gjson.Get(string(bytes), "list")
|
||||
|
||||
logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClsCalendar(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().ClsCalendar()
|
||||
md := strings.Builder{}
|
||||
for _, a := range res {
|
||||
bytes, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
//logger.SugaredLogger.Debugf("value: %+v", string(bytes))
|
||||
date := gjson.Get(string(bytes), "calendar_day")
|
||||
md.WriteString("\n### 事件/会议日期:" + date.String())
|
||||
list := gjson.Get(string(bytes), "items")
|
||||
//logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
|
||||
list.ForEach(func(key, value gjson.Result) bool {
|
||||
logger.SugaredLogger.Debugf("key: %+v,value: %+v", key.String(), gjson.Get(value.String(), "title"))
|
||||
md.WriteString("\n- " + gjson.Get(value.String(), "title").String())
|
||||
return true
|
||||
})
|
||||
}
|
||||
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")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,22 +8,50 @@ import (
|
||||
|
||||
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
var tools []Tool
|
||||
tools = append(tools, Tool{
|
||||
Type: "function",
|
||||
Function: ToolFunction{
|
||||
Name: "SearchStockByIndicators",
|
||||
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据",
|
||||
Parameters: FunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"words": map[string]any{
|
||||
"type": "string",
|
||||
"description": "选股自然语言,并且条件使用;分隔,或者条件使用,分隔。例如:创新药;PE<30;净利润增长率>50%;",
|
||||
},
|
||||
},
|
||||
Required: []string{"words"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ai := NewDeepSeekOpenAi(context.TODO())
|
||||
res := ai.NewChatStream("上海贝岭", "sh600171", "上海贝岭分析和总结", nil)
|
||||
//res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
|
||||
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险。调用工具函数验证", nil, tools)
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-res:
|
||||
t.Log(msg)
|
||||
if len(msg) > 0 {
|
||||
t.Log(msg)
|
||||
if msg["content"] == "DONE" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTopNewsList(t *testing.T) {
|
||||
GetTopNewsList(30)
|
||||
news := GetTopNewsList(30)
|
||||
t.Log(news)
|
||||
}
|
||||
|
||||
func TestSearchGuShiTongStockInfo(t *testing.T) {
|
||||
//db.Init("../../data/stock.db")
|
||||
db.Init("../../data/stock.db")
|
||||
SearchGuShiTongStockInfo("hk01810", 60)
|
||||
SearchGuShiTongStockInfo("sh600745", 60)
|
||||
SearchGuShiTongStockInfo("gb_goog", 60)
|
||||
|
||||
72
backend/data/search_stock_api.go
Normal file
72
backend/data/search_stock_api.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/6/28 21:02
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type SearchStockApi struct {
|
||||
words string
|
||||
}
|
||||
|
||||
func NewSearchStockApi(words string) *SearchStockApi {
|
||||
return &SearchStockApi{words: words}
|
||||
}
|
||||
func (s SearchStockApi) SearchStock(pageSize int) map[string]any {
|
||||
url := "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "np-tjxg-g.eastmoney.com").
|
||||
SetHeader("Origin", "https://xuangu.eastmoney.com").
|
||||
SetHeader("Referer", "https://xuangu.eastmoney.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(fmt.Sprintf(`{
|
||||
"keyWord": "%s",
|
||||
"pageSize": %d,
|
||||
"pageNo": 1,
|
||||
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
|
||||
"gids": [],
|
||||
"matchWord": "",
|
||||
"timestamp": "1751113883290349",
|
||||
"shareToGuba": false,
|
||||
"requestId": "8xTWgCDAjvQ5lmvz5mDA3Ydk2AE4yoiJ1751113883290",
|
||||
"needCorrect": true,
|
||||
"removedConditionIdList": [],
|
||||
"xcId": "xc0af28549ab330013ed",
|
||||
"ownSelectAll": false,
|
||||
"dxInfo": [],
|
||||
"extraCondition": ""
|
||||
}`, s.words, pageSize)).Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
|
||||
return map[string]any{}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap
|
||||
}
|
||||
|
||||
func (s SearchStockApi) HotStrategy() map[string]any {
|
||||
url := fmt.Sprintf("https://np-ipick.eastmoney.com/recommend/stock/heat/ranking?count=20&trace=%d&client=web&biz=web_smart_tag", time.Now().Unix())
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "np-ipick.eastmoney.com").
|
||||
SetHeader("Origin", "https://xuangu.eastmoney.com").
|
||||
SetHeader("Referer", "https://xuangu.eastmoney.com/").
|
||||
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("HotStrategy-err:%+v", err)
|
||||
return map[string]any{}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
return respMap
|
||||
}
|
||||
59
backend/data/search_stock_api_test.go
Normal file
59
backend/data/search_stock_api_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSearchStock(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock(10)
|
||||
data := res["data"].(map[string]any)
|
||||
result := data["result"].(map[string]any)
|
||||
dataList := result["dataList"].([]any)
|
||||
columns := result["columns"].([]any)
|
||||
headers := map[string]string{}
|
||||
for _, v := range columns {
|
||||
//logger.SugaredLogger.Infof("v:%+v", v)
|
||||
d := v.(map[string]any)
|
||||
//logger.SugaredLogger.Infof("key:%s title:%s dateMsg:%s unit:%s", d["key"], d["title"], d["dateMsg"], d["unit"])
|
||||
title := convertor.ToString(d["title"])
|
||||
if convertor.ToString(d["dateMsg"]) != "" {
|
||||
title = title + "[" + convertor.ToString(d["dateMsg"]) + "]"
|
||||
}
|
||||
if convertor.ToString(d["unit"]) != "" {
|
||||
title = title + "(" + convertor.ToString(d["unit"]) + ")"
|
||||
}
|
||||
headers[d["key"].(string)] = title
|
||||
}
|
||||
table := &[]map[string]any{}
|
||||
for _, v := range dataList {
|
||||
//logger.SugaredLogger.Infof("v:%+v", v)
|
||||
d := v.(map[string]any)
|
||||
tmp := map[string]any{}
|
||||
for key, title := range headers {
|
||||
//logger.SugaredLogger.Infof("%s:%s", title, convertor.ToString(d[key]))
|
||||
tmp[title] = convertor.ToString(d[key])
|
||||
}
|
||||
*table = append(*table, tmp)
|
||||
//logger.SugaredLogger.Infof("--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------")
|
||||
}
|
||||
jsonData, _ := json.Marshal(*table)
|
||||
markdownTable, _ := JSONToMarkdownTable(jsonData)
|
||||
logger.SugaredLogger.Infof("markdownTable=\n%s", markdownTable)
|
||||
}
|
||||
|
||||
func TestSearchStockApi_HotStrategy(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewSearchStockApi("").HotStrategy()
|
||||
logger.SugaredLogger.Infof("res:%+v", res)
|
||||
dataList := res["data"].([]any)
|
||||
for _, v := range dataList {
|
||||
d := v.(map[string]any)
|
||||
logger.SugaredLogger.Infof("v:%+v", d)
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,9 @@ type Settings struct {
|
||||
EnableNews bool `json:"enableNews"`
|
||||
DarkTheme bool `json:"darkTheme"`
|
||||
BrowserPoolSize int `json:"browserPoolSize"`
|
||||
EnableFund bool `json:"enableFund"`
|
||||
EnablePushNews bool `json:"enablePushNews"`
|
||||
SponsorCode string `json:"sponsorCode"`
|
||||
}
|
||||
|
||||
func (receiver Settings) TableName() string {
|
||||
@@ -76,6 +79,9 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
"browser_path": s.Config.BrowserPath,
|
||||
"enable_news": s.Config.EnableNews,
|
||||
"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)
|
||||
@@ -102,6 +108,9 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
BrowserPath: s.Config.BrowserPath,
|
||||
EnableNews: s.Config.EnableNews,
|
||||
DarkTheme: s.Config.DarkTheme,
|
||||
EnableFund: s.Config.EnableFund,
|
||||
EnablePushNews: s.Config.EnablePushNews,
|
||||
SponsorCode: s.Config.SponsorCode,
|
||||
})
|
||||
}
|
||||
return "保存成功!"
|
||||
@@ -122,10 +131,10 @@ func (s SettingsApi) GetConfig() *Settings {
|
||||
}
|
||||
}
|
||||
if settings.BrowserPath == "" {
|
||||
settings.BrowserPath, _ = CheckBrowserOnWindows()
|
||||
settings.BrowserPath, _ = CheckBrowser()
|
||||
}
|
||||
if settings.BrowserPoolSize <= 0 {
|
||||
settings.BrowserPoolSize = 3
|
||||
settings.BrowserPoolSize = 1
|
||||
}
|
||||
return &settings
|
||||
}
|
||||
|
||||
1
backend/data/stock_basic.json
Normal file
1
backend/data/stock_basic.json
Normal file
File diff suppressed because one or more lines are too long
@@ -8,7 +8,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
@@ -16,20 +15,24 @@ import (
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/samber/lo"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const sinaStockUrl = "http://hq.sinajs.cn/rn=%d&list=%s"
|
||||
const txStockUrl = "http://qt.gtimg.cn/?_=%d&q=%s"
|
||||
|
||||
const tushareApiUrl = "http://api.tushare.pro"
|
||||
|
||||
type StockDataApi struct {
|
||||
@@ -90,6 +93,8 @@ type StockInfo struct {
|
||||
Sort int64 `json:"sort"` //排序
|
||||
AlarmChangePercent float64 `json:"alarmChangePercent"`
|
||||
AlarmPrice float64 `json:"alarmPrice"`
|
||||
|
||||
Groups []GroupStock `gorm:"-:all"`
|
||||
}
|
||||
|
||||
func (receiver StockInfo) TableName() string {
|
||||
@@ -148,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 {
|
||||
@@ -162,8 +169,9 @@ type FollowedStock struct {
|
||||
AlarmPrice float64
|
||||
Time time.Time
|
||||
Sort int64
|
||||
Cron string
|
||||
Cron *string
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
Groups []GroupStock `gorm:"foreignKey:StockCode;references:StockCode"`
|
||||
}
|
||||
|
||||
func (receiver FollowedStock) TableName() string {
|
||||
@@ -241,7 +249,7 @@ func (receiver StockDataApi) GetIndexBasic() {
|
||||
func (receiver StockDataApi) GetStockBaseInfo() {
|
||||
res := &TushareStockBasicResponse{}
|
||||
fields := "ts_code,symbol,name,area,industry,cnspell,market,list_date,act_name,act_ent_type,fullname,exchange,list_status,curr_type,enname,delist_date,is_hs"
|
||||
_, err := receiver.client.R().
|
||||
resp, err := receiver.client.R().
|
||||
SetHeader("content-type", "application/json").
|
||||
SetBody(&TushareRequest{
|
||||
ApiName: "stock_basic",
|
||||
@@ -252,8 +260,7 @@ func (receiver StockDataApi) GetStockBaseInfo() {
|
||||
SetResult(res).
|
||||
Post(tushareApiUrl)
|
||||
//logger.SugaredLogger.Infof("GetStockBaseInfo %s", string(resp.Body()))
|
||||
//resp.Body()写入文件
|
||||
//ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
|
||||
ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
|
||||
//logger.SugaredLogger.Infof("GetStockBaseInfo %+v", res)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
@@ -286,8 +293,58 @@ func (receiver StockDataApi) GetStockBaseInfo() {
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]StockInfo, error) {
|
||||
stockInfos := make([]StockInfo, 0)
|
||||
|
||||
codes := slice.JoinFunc(StockCodes, ",", func(s string) string {
|
||||
hkcodes := slice.Filter(StockCodes, func(i int, s string) bool {
|
||||
return strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
|
||||
})
|
||||
|
||||
if hkcodes != nil && len(hkcodes) > 0 {
|
||||
hkcodesStr := slice.JoinFunc(hkcodes, ",", func(s string) string {
|
||||
if strutil.HasPrefixAny(s, []string{"hk", "HK"}) {
|
||||
return "r_" + strings.ToLower(s)
|
||||
} else {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
})
|
||||
url := fmt.Sprintf(txStockUrl, time.Now().Unix(), hkcodesStr)
|
||||
resp, err := receiver.client.R().
|
||||
SetHeader("Host", "qt.gtimg.cn").
|
||||
SetHeader("Referer", "https://gu.qq.com/").
|
||||
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(url)
|
||||
logger.SugaredLogger.Infof("GetStockCodeRealTimeData %s", url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]StockInfo{}, err
|
||||
}
|
||||
str := GB18030ToUTF8(resp.Body())
|
||||
dataStr := strutil.SplitAndTrim(strings.Trim(str, "\n"), ";")
|
||||
|
||||
for _, data := range dataStr {
|
||||
stockData, err := ParseTxStockData(data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
stockInfos = append(stockInfos, *stockData)
|
||||
go func() {
|
||||
var count int64
|
||||
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Count(&count)
|
||||
if count == 0 {
|
||||
db.Dao.Model(&StockInfo{}).Create(stockData)
|
||||
} else {
|
||||
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Updates(stockData)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
szzsusCodes := slice.Filter(StockCodes, func(i int, s string) bool {
|
||||
return !strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
|
||||
})
|
||||
|
||||
codes := slice.JoinFunc(szzsusCodes, ",", func(s string) string {
|
||||
if strings.HasPrefix(s, "us") {
|
||||
s = strings.Replace(s, "us", "gb_", 1)
|
||||
}
|
||||
@@ -309,12 +366,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
|
||||
return &[]StockInfo{}, err
|
||||
}
|
||||
|
||||
stockInfos := make([]StockInfo, 0)
|
||||
str := GB18030ToUTF8(resp.Body())
|
||||
dataStr := strutil.SplitEx(str, "\n", true)
|
||||
if len(dataStr) == 0 {
|
||||
return &[]StockInfo{}, errors.New("获取股票信息失败,请检查股票代码是否正确")
|
||||
}
|
||||
|
||||
for _, data := range dataStr {
|
||||
//logger.SugaredLogger.Info(data)
|
||||
stockData, err := ParseFullSingleStockData(data)
|
||||
@@ -347,7 +401,20 @@ func (receiver StockDataApi) Follow(stockCode string) string {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return "关注失败"
|
||||
}
|
||||
if strings.HasPrefix(stockCode, "us") {
|
||||
stockCode = strings.Replace(stockCode, "us", "gb_", 1)
|
||||
}
|
||||
if strings.HasPrefix(stockCode, "US") {
|
||||
stockCode = strings.Replace(stockCode, "US", "gb_", 1)
|
||||
}
|
||||
count := int64(0)
|
||||
db.Dao.Model(&FollowedStock{}).Where("is_del = ?", 0).Count(&count)
|
||||
logger.SugaredLogger.Errorf("Follow-count %v", count)
|
||||
if count >= 63 {
|
||||
return "最多只能关注63只股票"
|
||||
}
|
||||
|
||||
stockCode = strings.ToLower(stockCode)
|
||||
maxSort := int64(0)
|
||||
db.Dao.Model(&FollowedStock{}).Raw("select max(sort) as sort from followed_stock").Scan(&maxSort)
|
||||
|
||||
@@ -410,15 +477,64 @@ func (receiver StockDataApi) SetAlarmChangePercent(val, alarmPrice float64, stoc
|
||||
return "设置成功"
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) SetStockSort(sort int64, stockCode string) {
|
||||
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
|
||||
stockCode = strings.ToLower(stockCode)
|
||||
stockCode = strings.Replace(stockCode, "gb_", "us", 1)
|
||||
func (receiver StockDataApi) SetStockSort(newSort int64, stockCode string) {
|
||||
//if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
|
||||
// stockCode = strings.ToLower(stockCode)
|
||||
// stockCode = strings.Replace(stockCode, "gb_", "us", 1)
|
||||
//}
|
||||
|
||||
// 获取当前排序值
|
||||
var currentStock FollowedStock
|
||||
if err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).First(¤tStock).Error; err != nil {
|
||||
logger.SugaredLogger.Error("找不到当前股票: ", err.Error())
|
||||
return
|
||||
}
|
||||
err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("sort", sort).Error
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
|
||||
oldSort := currentStock.Sort
|
||||
|
||||
// 如果排序值没有变化,直接返回
|
||||
if oldSort == newSort {
|
||||
return
|
||||
}
|
||||
// 检查新排序位置是否被占用
|
||||
var count int64
|
||||
if err := db.Dao.Model(&FollowedStock{}).Where("sort = ?", newSort).Count(&count).Error; err != nil {
|
||||
logger.SugaredLogger.Error("检查新排序位置被占用失败: ", err.Error())
|
||||
return
|
||||
}
|
||||
if count == 0 {
|
||||
// 新位置未被占用,直接更新当前记录
|
||||
if err := db.Dao.Model(&FollowedStock{}).
|
||||
Where("stock_code = ?", strings.ToLower(stockCode)).
|
||||
Update("sort", newSort).Error; err != nil {
|
||||
logger.SugaredLogger.Error("更新排序位置失败: ", err.Error())
|
||||
}
|
||||
} else {
|
||||
// 新位置已被占用,需要移动其他记录
|
||||
if newSort < oldSort {
|
||||
// 向前移动:将中间记录向后移动
|
||||
if err := db.Dao.Model(&FollowedStock{}).
|
||||
Where("sort >= ? AND sort < ?", newSort, oldSort).
|
||||
Update("sort", gorm.Expr("sort + 1")).Error; err != nil {
|
||||
logger.SugaredLogger.Error("向前排序更新失败: ", err.Error())
|
||||
}
|
||||
} else {
|
||||
// 向后移动:将中间记录向前移动
|
||||
if err := db.Dao.Model(&FollowedStock{}).
|
||||
Where("sort > ? AND sort <= ?", oldSort, newSort).
|
||||
Update("sort", gorm.Expr("sort - 1")).Error; err != nil {
|
||||
logger.SugaredLogger.Error("向后排序更新失败: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 更新目标记录的排序
|
||||
if err := db.Dao.Model(&FollowedStock{}).
|
||||
Where("stock_code = ?", strings.ToLower(stockCode)).
|
||||
Update("sort", newSort).Error; err != nil {
|
||||
logger.SugaredLogger.Error("更新股票排序失败: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func (receiver StockDataApi) SetStockAICron(cron string, stockCode string) {
|
||||
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
|
||||
@@ -429,9 +545,20 @@ func (receiver StockDataApi) SetStockAICron(cron string, stockCode string) {
|
||||
db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("cron", cron)
|
||||
|
||||
}
|
||||
func (receiver StockDataApi) GetFollowList() *[]FollowedStock {
|
||||
func (receiver StockDataApi) GetFollowList(groupId int) *[]FollowedStock {
|
||||
logger.SugaredLogger.Infof("GetFollowList %d", groupId)
|
||||
|
||||
var result *[]FollowedStock
|
||||
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)
|
||||
if groupId == 0 {
|
||||
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)
|
||||
} else {
|
||||
infos := NewStockGroupApi(db.Dao).GetGroupStockByGroupId(groupId)
|
||||
codes := lo.FlatMap(infos, func(info GroupStock, idx int) []string {
|
||||
return []string{info.StockCode}
|
||||
})
|
||||
db.Dao.Model(&FollowedStock{}).Where("stock_code in ?", codes).Order("sort asc,time desc").Find(&result)
|
||||
logger.SugaredLogger.Infof("GetFollowList %+v", result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -493,6 +620,146 @@ func GB18030ToUTF8(bs []byte) string {
|
||||
return string(d)
|
||||
}
|
||||
|
||||
func ParseTxStockData(data string) (*StockInfo, error) {
|
||||
//v_r_hk09660="100~地平线机器人-W~09660~6.240~5.690~5.800~192659034.0~0~0~6.240~0~0~0~0~0~0~0~0~0~6.240~0~0~0~0~0~0~0~0~0~192659034.0~2025/04/29
|
||||
//13:41:04~0.550~9.67~6.450~5.710~6.240~192659034.0~1180471843.140~0~32.51~~0~0~13.01~691.1364~823.6983~HORIZONROBOT-W~0.00~10.380~3.320~1.07~-16.03~0~0~0~0~0~32.51~6.40~1.74~600~73.33~17.96~GP~19.70~11.51~-0.95~-18.54~44.44~13200293682.00~11075904412.00~32.51~0.000~6.127~56.39~HKD~1~30";
|
||||
//v_sz002241="51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~
|
||||
//~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0";
|
||||
|
||||
datas := strutil.SplitAndTrim(data, "=", "\"")
|
||||
if len(datas) < 2 {
|
||||
return nil, fmt.Errorf("invalid data format")
|
||||
}
|
||||
var result map[string]string
|
||||
var err error
|
||||
if strutil.ContainsAny(datas[0], []string{"v_r_hk", "v_hk", "v_sz", "v_sh"}) {
|
||||
result, err = ParseTxHKStockData(datas)
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
|
||||
marshal, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成marshal: %s", marshal)
|
||||
stockInfo := &StockInfo{}
|
||||
err = json.Unmarshal(marshal, &stockInfo)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成stockInfo: %+v", stockInfo)
|
||||
|
||||
return stockInfo, nil
|
||||
|
||||
}
|
||||
|
||||
func ParseTxHKStockData(datas []string) (map[string]string, error) {
|
||||
//v_r_hk09660="
|
||||
//100~ 0
|
||||
//地平线机器人-W~ 1
|
||||
//09660~ 2
|
||||
//6.270~ 3 当前价
|
||||
//5.690~ 4 昨收价
|
||||
//5.800~ 5 开盘价
|
||||
//195083034.0~
|
||||
//0~
|
||||
//0~
|
||||
//6.270~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//6.270~
|
||||
//0~0~0~0~0~0~0~0~0~
|
||||
//195083034.0~
|
||||
//2025/04/29 13:45:41~ 30 当前时间
|
||||
//0.580~
|
||||
//10.19~
|
||||
//6.450~ 最高价
|
||||
//5.710~ 最低价
|
||||
//6.270~
|
||||
//195083034.0~
|
||||
//1195673623.140~
|
||||
//0~
|
||||
//32.66
|
||||
//~~0~0~13.01~694.4592~827.6584~HORIZONROBOT-W~0.00~10.380~3.320~1.06~-18.71~0~0~0~0~0~32.66~6.43~1.76~600~74.17~18.53~GP~19.70~11.51~-0.48~-18.15~45.14~13200293682.00~11075904412.00~32.66~0.000~6.129~57.14~HKD~1~30";
|
||||
result := make(map[string]string)
|
||||
|
||||
stockCode := strutil.ReplaceWithMap(datas[0], map[string]string{
|
||||
"v_r_": "",
|
||||
"v_": "",
|
||||
})
|
||||
result["股票代码"] = stockCode
|
||||
|
||||
parts := strutil.SplitAndTrim(datas[1], "~")
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成 len: %v", len(parts))
|
||||
if len(parts) < 35 {
|
||||
return nil, fmt.Errorf("invalid data format")
|
||||
}
|
||||
result["股票名称"] = parts[1]
|
||||
result["当前价格"] = parts[3]
|
||||
result["昨日收盘价"] = parts[4]
|
||||
result["今日开盘价"] = parts[5]
|
||||
|
||||
result["今日最高价"] = parts[33]
|
||||
result["今日最低价"] = parts[34]
|
||||
|
||||
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
|
||||
result["买一报价"] = parts[9]
|
||||
result["买一申报"] = parts[10]
|
||||
result["买二报价"] = parts[11]
|
||||
result["买二申报"] = parts[12]
|
||||
result["买三报价"] = parts[13]
|
||||
result["买三申报"] = parts[14]
|
||||
result["买四报价"] = parts[15]
|
||||
result["买四申报"] = parts[16]
|
||||
result["买五报价"] = parts[17]
|
||||
result["买五申报"] = parts[18]
|
||||
|
||||
result["卖一报价"] = parts[19]
|
||||
result["卖一申报"] = parts[20]
|
||||
result["卖二报价"] = parts[21]
|
||||
result["卖二申报"] = parts[22]
|
||||
result["卖三报价"] = parts[23]
|
||||
result["卖三申报"] = parts[24]
|
||||
result["卖四报价"] = parts[25]
|
||||
result["卖四申报"] = parts[26]
|
||||
result["卖五报价"] = parts[27]
|
||||
result["卖五申报"] = parts[28]
|
||||
|
||||
}
|
||||
|
||||
timestr := ""
|
||||
|
||||
if strutil.ContainsAny(parts[30], []string{"/"}) {
|
||||
timestr = strutil.ReplaceWithMap(parts[30], map[string]string{
|
||||
"/": "-",
|
||||
"\n": " ",
|
||||
})
|
||||
result["日期"] = strutil.SplitAndTrim(timestr, " ", "")[0]
|
||||
result["时间"] = strutil.SplitAndTrim(timestr, " ", "")[1]
|
||||
} else {
|
||||
result["日期"] = strutil.Trim(parts[29])[0:4] + "-" + strutil.Trim(parts[29])[4:6] + "-" + strutil.Trim(parts[29])[6:8]
|
||||
result["时间"] = strutil.Trim(parts[29])[8:10] + ":" + strutil.Trim(parts[29])[10:12] + ":" + strutil.Trim(parts[29])[12:14]
|
||||
result["今日最高价"] = parts[32]
|
||||
result["今日最低价"] = parts[33]
|
||||
}
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成 %s %s 时间: %s,%s", parts[1], parts[3], parts[29], parts[30])
|
||||
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成 时间: %v", timestr)
|
||||
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ParseFullSingleStockData(data string) (*StockInfo, error) {
|
||||
datas := strutil.SplitAndTrim(data, "=", "\"")
|
||||
if len(datas) < 2 {
|
||||
@@ -768,7 +1035,7 @@ func GetRealTimeStockPriceInfo(ctx context.Context, stockCode string) (price, pr
|
||||
return price, priceTime
|
||||
}
|
||||
|
||||
func SearchStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
func SearchStockPriceInfo(stockName, stockCode string, crawlTimeOut int64) *[]string {
|
||||
|
||||
if strutil.HasPrefixAny(stockCode, []string{"SZ", "SH", "sh", "sz", "bj"}) {
|
||||
//if strutil.HasPrefixAny(stockCode, []string{"bj", "BJ"}) {
|
||||
@@ -778,7 +1045,7 @@ func SearchStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
// }) + ".BJ"
|
||||
//}
|
||||
|
||||
return getSHSZStockPriceInfo(stockCode, crawlTimeOut)
|
||||
return getSHSZStockPriceInfo(stockName, stockCode, crawlTimeOut)
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
|
||||
return getHKStockPriceInfo(stockCode, crawlTimeOut)
|
||||
@@ -838,6 +1105,7 @@ func getUSStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
messages = append(messages, text)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof("messages: %s", messages)
|
||||
return &messages
|
||||
}
|
||||
|
||||
@@ -855,10 +1123,12 @@ func getHKStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
url := fmt.Sprintf("https://stock.finance.sina.com.cn/hkstock/quotes/%s.html", strings.ReplaceAll(stockCode, "hk", ""))
|
||||
htmlContent, ok := crawlerAPI.GetHtml(url, ".deta_hqContainer >.deta03 ", true)
|
||||
logger.SugaredLogger.Infof("CrawlHKStockPriceInfo url:%s", url)
|
||||
htmlContent, ok := crawlerAPI.GetHtml(url, "div.deta_hqContainer >.deta03>ul ", false)
|
||||
if !ok {
|
||||
return &[]string{}
|
||||
}
|
||||
//logger.SugaredLogger.Infof("CrawlHKStockPriceInfo htmlContent:%s", htmlContent)
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
@@ -890,6 +1160,7 @@ func getHKStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
messages = append(messages, text)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof("messages: %s", messages)
|
||||
return &messages
|
||||
}
|
||||
|
||||
@@ -919,12 +1190,12 @@ func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
|
||||
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString(fmt.Sprintf("### %s:%s 时间:%s\n", name, price, hqTime))
|
||||
markdown.WriteString(fmt.Sprintf("### 时间:%s %s:%s \n", hqTime, name, price))
|
||||
GetTableMarkdown(document, "div#hqDetails table", &markdown)
|
||||
return markdown.String()
|
||||
}
|
||||
|
||||
func getSHSZStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
func getSHSZStockPriceInfo(stockName, stockCode string, crawlTimeOut int64) *[]string {
|
||||
url := "https://finance.sina.com.cn/realstock/company/" + stockCode + "/nc.shtml"
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
@@ -950,7 +1221,7 @@ func getSHSZStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString(fmt.Sprintf("### 当前股价:%s 时间:%s\n", price, hqTime))
|
||||
markdown.WriteString(fmt.Sprintf("### %s现价:%s 现价时间:%s\n", stockName, price, hqTime))
|
||||
GetTableMarkdown(document, "div#hqDetails table", &markdown)
|
||||
return &[]string{markdown.String()}
|
||||
}
|
||||
@@ -1028,46 +1299,492 @@ func SearchStockInfoByCode(stock string) *[]string {
|
||||
return &messages
|
||||
}
|
||||
|
||||
// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装
|
||||
func checkChromeOnWindows() (string, bool) {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
// 分时数据
|
||||
func (receiver StockDataApi) GetStockMinutePriceData(stockCode string) (*[]MinuteData, string) {
|
||||
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/minute/query?code=%s", stockCode)
|
||||
if strutil.HasPrefixAny(stockCode, []string{"gb_", "GB_"}) {
|
||||
stockCode = strings.Replace(strings.ToUpper(stockCode), "GB_", "us", 1) + ".OQ"
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"us", "US"}) {
|
||||
url = fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/UsMinute/query?code=%s", stockCode)
|
||||
}
|
||||
logger.SugaredLogger.Infof("GetStockMinutePriceData url:%s", url)
|
||||
res := make(map[string]interface{})
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "web.ifzq.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(url)
|
||||
|
||||
date := ""
|
||||
minuteDatas := &[]MinuteData{}
|
||||
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", false
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return minuteDatas, date
|
||||
}
|
||||
//logger.SugaredLogger.Infof("resp:%s", resp.Body())
|
||||
json.Unmarshal(resp.Body(), &res)
|
||||
code, _ := convertor.ToInt(res["code"])
|
||||
if res["data"] != nil && code == 0 {
|
||||
data := res["data"].(map[string]interface{})
|
||||
if stockData, ok := data[stockCode]; ok {
|
||||
m := stockData.(map[string]interface{})
|
||||
if d, ok := m["data"]; ok {
|
||||
if m2, ok := d.(map[string]any); ok {
|
||||
minutePriceData := m2["data"]
|
||||
datas := minutePriceData.([]any)
|
||||
for _, item := range datas {
|
||||
minuteDataSplit := strutil.SplitEx(strutil.ReplaceWithMap(item.(string), map[string]string{
|
||||
"\r\n": " ",
|
||||
}), " ", true)
|
||||
price, _ := convertor.ToFloat(minuteDataSplit[1])
|
||||
volume, _ := convertor.ToFloat(minuteDataSplit[2])
|
||||
amount := float64(0)
|
||||
if len(minuteDataSplit) >= 4 {
|
||||
amount, _ = convertor.ToFloat(minuteDataSplit[3])
|
||||
}
|
||||
minuteData := &MinuteData{
|
||||
Time: minuteDataSplit[0][0:2] + ":" + minuteDataSplit[0][2:4],
|
||||
Price: price,
|
||||
Volume: volume,
|
||||
Amount: amount,
|
||||
}
|
||||
*minuteDatas = append(*minuteDatas, *minuteData)
|
||||
}
|
||||
date = m2["date"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
defer key.Close()
|
||||
path, _, err := key.GetStringValue("Path")
|
||||
//logger.SugaredLogger.Infof("Chrome安装路径:%s", path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return path + "\\chrome.exe", true
|
||||
return minuteDatas, date
|
||||
}
|
||||
|
||||
// CheckBrowserOnWindows 在 Windows 系统上检查Edge浏览器是否安装,并返回安装路径
|
||||
func CheckBrowserOnWindows() (string, bool) {
|
||||
if path, ok := checkChromeOnWindows(); ok {
|
||||
return path, true
|
||||
func (receiver StockDataApi) GetKLineData(stockCode string, kLineType string, days int64) *[]KLineData {
|
||||
url := fmt.Sprintf("http://quotes.sina.cn/cn/api/json_v2.php/CN_MarketDataService.getKLineData?symbol=%s&scale=%s&ma=yes&datalen=%d", stockCode, kLineType, days)
|
||||
K := &[]KLineData{}
|
||||
_, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "quotes.sina.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
SetResult(K).
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return K
|
||||
}
|
||||
return K
|
||||
}
|
||||
func (receiver StockDataApi) GetHK_KLineData(stockCode string, kLineType string, days int64) *[]KLineData {
|
||||
|
||||
logger.SugaredLogger.Infof("GetHK_KLineData stockCode:%s,kLineType:%s,days:%d", stockCode, kLineType, days)
|
||||
if strutil.HasPrefixAny(stockCode, []string{"gb_", "GB_"}) {
|
||||
stockCode = strings.Replace(stockCode, "gb_", "us", 1) + ".OQ"
|
||||
}
|
||||
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=%s,%s,,,%d,qfq", stockCode, kLineType, days)
|
||||
//logger.SugaredLogger.Infof("url:%s", url)
|
||||
K := &[]KLineData{}
|
||||
res := make(map[string]interface{})
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "web.ifzq.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", false
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return K
|
||||
}
|
||||
//logger.SugaredLogger.Infof("resp:%s", resp.Body())
|
||||
json.Unmarshal(resp.Body(), &res)
|
||||
code, _ := convertor.ToInt(res["code"])
|
||||
if code != 0 {
|
||||
return K
|
||||
}
|
||||
if res["data"] != nil && code == 0 {
|
||||
data := res["data"].(map[string]interface{})[stockCode].(map[string]interface{})
|
||||
if data != nil {
|
||||
var day []any
|
||||
if data["qfqday"] != nil {
|
||||
day = data["qfqday"].([]any)
|
||||
}
|
||||
if data["day"] != nil {
|
||||
day = data["day"].([]any)
|
||||
}
|
||||
for _, v := range day {
|
||||
if v != nil {
|
||||
vv := v.([]any)
|
||||
KLine := &KLineData{
|
||||
Day: convertor.ToString(vv[0]),
|
||||
Open: convertor.ToString(vv[1]),
|
||||
Close: convertor.ToString(vv[2]),
|
||||
High: convertor.ToString(vv[3]),
|
||||
Low: convertor.ToString(vv[4]),
|
||||
Volume: convertor.ToString(vv[5]),
|
||||
}
|
||||
*K = append(*K, *KLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
defer key.Close()
|
||||
path, _, err := key.GetStringValue("Path")
|
||||
//logger.SugaredLogger.Infof("Edge安装路径:%s", path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return path + "\\msedge.exe", true
|
||||
return K
|
||||
}
|
||||
func (receiver StockDataApi) GetSinaHKStockInfo() {
|
||||
|
||||
pageSize := 500
|
||||
for i := 1; i <= 3060/pageSize; i++ {
|
||||
infos := getSinaStockInfo(receiver, i, pageSize)
|
||||
for i, info := range *infos {
|
||||
logger.SugaredLogger.Infof("infos:%d,%s:%s", i, info.Symbol, info.Name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getSinaStockInfo(receiver StockDataApi, page, pageSize int) *[]models.SinaStockInfo {
|
||||
infos := &[]models.SinaStockInfo{}
|
||||
url := "https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHKStockData?page=%d&num=%d&sort=symbol&asc=1&node=qbgg_hk&_s_r_a=init"
|
||||
_, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).SetProxy("http://localhost:10809").R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
SetResult(infos).
|
||||
Get(fmt.Sprintf(url, page, pageSize))
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
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 := "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"
|
||||
case "us":
|
||||
fs = "m:105,m:106,m:107"
|
||||
}
|
||||
|
||||
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(sprintfUrl)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
body := string(resp.Body())
|
||||
logger.SugaredLogger.Infof("resp:%s", body)
|
||||
vm := otto.New()
|
||||
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.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"].([]any)
|
||||
logger.SugaredLogger.Infof("total:%d", int(total))
|
||||
for k, item := range diff {
|
||||
stock := item.(map[string]any)
|
||||
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),
|
||||
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),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func DCToTsCode(dcCode string) string {
|
||||
//北京证券交易所 8(83、87、88 等) 创新型中小企业(专精特新为主)
|
||||
//上海证券交易所 6(60、688 等) 大盘蓝筹、科创板(高新技术)
|
||||
//深圳证券交易所 0、3(000、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().
|
||||
SetHeader("Host", "stock.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(fmt.Sprintf(url, pageSize))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
js := "var " + string(resp.Body())
|
||||
vm := otto.New()
|
||||
_, err = vm.Run(js)
|
||||
_, err = vm.Run("var data = JSON.stringify(list_data);")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value, err := vm.Get("data")
|
||||
data := make(map[string]any)
|
||||
err = json.Unmarshal([]byte(value.String()), &data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("resp:%s", data)
|
||||
if data["code"] != nil && data["code"].(float64) == 0 {
|
||||
d := data["data"].(map[string]any)
|
||||
saveHKStockInfo(d)
|
||||
|
||||
page_count := int64(d["page_count"].(float64))
|
||||
logger.SugaredLogger.Infof("page_count:%d", page_count)
|
||||
page := int64(1)
|
||||
for page > page_count {
|
||||
urlx := fmt.Sprintf("https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=%d&order=desc&var_name=list_data", pageSize, page)
|
||||
logger.SugaredLogger.Infof("url:%s", urlx)
|
||||
resp, err = receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "stock.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(urlx)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
break
|
||||
}
|
||||
js = "var " + string(resp.Body())
|
||||
_, err = vm.Run(js)
|
||||
_, err = vm.Run("var data = JSON.stringify(list_data);")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value, err = vm.Get("data")
|
||||
data = make(map[string]any)
|
||||
err = json.Unmarshal([]byte(value.String()), &data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("resp:%s", data)
|
||||
if data != nil && data["code"] != nil && data["code"].(float64) == 0 {
|
||||
if data["data"] != nil {
|
||||
d = data["data"].(map[string]any)
|
||||
saveHKStockInfo(d)
|
||||
}
|
||||
}
|
||||
page++
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func saveHKStockInfo(d map[string]any) {
|
||||
for _, v := range d["page_data"].([]any) {
|
||||
vv := v.(string)
|
||||
splits := strings.Split(vv, "~")
|
||||
stock := &models.StockInfoHK{
|
||||
Code: strutil.PadStart(splits[0], 5, "0") + ".HK",
|
||||
Name: splits[1],
|
||||
}
|
||||
logger.SugaredLogger.Infof("vv:%s", vv)
|
||||
db.Dao.Model(stock).Where("code = ?", stock.Code).First(stock)
|
||||
if stock.ID == 0 {
|
||||
logger.SugaredLogger.Infof("stock:%+v", stock)
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(stock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetCommonKLineData(stockCode string, kLineType string, days int64) *[]KLineData {
|
||||
|
||||
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=%s,%s,,,%d,qfq", stockCode, kLineType, days)
|
||||
logger.SugaredLogger.Infof("url:%s", url)
|
||||
K := &[]KLineData{}
|
||||
res := make(map[string]interface{})
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "web.ifzq.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return K
|
||||
}
|
||||
logger.SugaredLogger.Infof("resp:%s", resp.Body())
|
||||
json.Unmarshal(resp.Body(), &res)
|
||||
code, _ := convertor.ToInt(res["code"])
|
||||
if code != 0 {
|
||||
return K
|
||||
}
|
||||
if res["data"] != nil && code == 0 {
|
||||
data := res["data"].(map[string]interface{})[stockCode].(map[string]interface{})
|
||||
if data != nil {
|
||||
var day []any
|
||||
if data["qfqday"] != nil {
|
||||
day = data["qfqday"].([]any)
|
||||
}
|
||||
if data["day"] != nil {
|
||||
day = data["day"].([]any)
|
||||
}
|
||||
for _, v := range day {
|
||||
if v != nil {
|
||||
vv := v.([]any)
|
||||
KLine := &KLineData{
|
||||
Day: convertor.ToString(vv[0]),
|
||||
Open: convertor.ToString(vv[1]),
|
||||
Close: convertor.ToString(vv[2]),
|
||||
High: convertor.ToString(vv[3]),
|
||||
Low: convertor.ToString(vv[4]),
|
||||
Volume: convertor.ToString(vv[5]),
|
||||
}
|
||||
*K = append(*K, *KLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return K
|
||||
}
|
||||
|
||||
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
|
||||
func JSONToMarkdownTable(jsonData []byte) (string, error) {
|
||||
var data []map[string]interface{}
|
||||
err := json.Unmarshal(jsonData, &data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 获取表头
|
||||
headers := []string{}
|
||||
for key := range data[0] {
|
||||
headers = append(headers, key)
|
||||
}
|
||||
|
||||
// 构建表头行
|
||||
headerRow := "|"
|
||||
for _, header := range headers {
|
||||
headerRow += fmt.Sprintf(" %s |", header)
|
||||
}
|
||||
headerRow += "\n"
|
||||
|
||||
// 构建分隔行
|
||||
separatorRow := "|"
|
||||
for range headers {
|
||||
separatorRow += " --- |"
|
||||
}
|
||||
separatorRow += "\n"
|
||||
|
||||
// 构建数据行
|
||||
bodyRows := ""
|
||||
for _, rowData := range data {
|
||||
bodyRow := "|"
|
||||
for _, header := range headers {
|
||||
value := rowData[header]
|
||||
bodyRow += fmt.Sprintf(" %v |", value)
|
||||
}
|
||||
bodyRows += bodyRow + "\n"
|
||||
}
|
||||
|
||||
return headerRow + separatorRow + bodyRows, nil
|
||||
}
|
||||
|
||||
type KLineData struct {
|
||||
Day string `json:"day"`
|
||||
Open string `json:"open"`
|
||||
High string `json:"high"`
|
||||
Low string `json:"low"`
|
||||
Close string `json:"close"`
|
||||
Volume string `json:"volume"`
|
||||
}
|
||||
|
||||
type MinuteData struct {
|
||||
Time string `json:"time"`
|
||||
Price float64 `json:"price"`
|
||||
Volume float64 `json:"volume"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
37
backend/data/stock_data_api_darwin.go
Normal file
37
backend/data/stock_data_api_darwin.go
Normal file
@@ -0,0 +1,37 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package data
|
||||
|
||||
import "os"
|
||||
|
||||
// CheckChrome 检查 macOS 是否安装了 Chrome 浏览器
|
||||
func CheckChrome() (string, bool) {
|
||||
// 检查 /Applications 目录下是否存在 Chrome
|
||||
locations := []string{
|
||||
// Mac
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
}
|
||||
path := ""
|
||||
for _, location := range locations {
|
||||
_, err := os.Stat(location)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
path = location
|
||||
}
|
||||
if path == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return path, true
|
||||
}
|
||||
|
||||
// CheckBrowser 检查 macOS 是否安装了浏览器,并返回安装路径
|
||||
func CheckBrowser() (string, bool) {
|
||||
if path, ok := CheckChrome(); ok {
|
||||
return path, ok
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
@@ -22,17 +23,31 @@ import (
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestGetTelegraph(t *testing.T) {
|
||||
GetTelegraphList(30)
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
//telegraphs := GetTelegraphList(30)
|
||||
//for _, telegraph := range *telegraphs {
|
||||
// logger.SugaredLogger.Info(telegraph)
|
||||
//}
|
||||
list := NewMarketNewsApi().GetNewTelegraph(30)
|
||||
for _, telegraph := range *list {
|
||||
logger.SugaredLogger.Infof("telegraph:%+v", telegraph)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFinancialReports(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
//GetFinancialReports("sz000802", 30)
|
||||
//GetFinancialReports("hk00927", 30)
|
||||
GetFinancialReports("gb_aapl", 30)
|
||||
//GetFinancialReports("gb_aapl", 30)
|
||||
GetFinancialReportsByXUEQIU("sz000802", 30)
|
||||
GetFinancialReportsByXUEQIU("gb_aapl", 30)
|
||||
GetFinancialReportsByXUEQIU("hk00927", 30)
|
||||
|
||||
}
|
||||
|
||||
func TestGetTelegraphSearch(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
|
||||
messages := SearchStockInfo("谷歌", "telegram", 30)
|
||||
for _, message := range *messages {
|
||||
@@ -42,20 +57,74 @@ func TestGetTelegraphSearch(t *testing.T) {
|
||||
//https://www.cls.cn/stock?code=sh600745
|
||||
}
|
||||
func TestSearchStockInfoByCode(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
SearchStockInfoByCode("sh600745")
|
||||
}
|
||||
|
||||
func TestSearchStockPriceInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
//SearchStockPriceInfo("hk06030", 30)
|
||||
//SearchStockPriceInfo("sh600171", 30)
|
||||
//SearchStockPriceInfo("gb_aapl", 30)
|
||||
//SearchStockPriceInfo("bj430198", 30)
|
||||
getZSInfo("创业板指数", "sz399006", 30)
|
||||
getZSInfo("上证综合指数", "sh000001", 30)
|
||||
getZSInfo("沪深300指数", "sh000300", 30)
|
||||
//SearchStockPriceInfo("中信证券", "hk06030", 30)
|
||||
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
|
||||
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
|
||||
//SearchStockPriceInfo("微创光电", "bj430198", 30)
|
||||
//getZSInfo("创业板指数", "sz399006", 30)
|
||||
//getZSInfo("上证综合指数", "sh000001", 30)
|
||||
//getZSInfo("沪深300指数", "sh000300", 30)
|
||||
|
||||
}
|
||||
func TestGetStockMinutePriceData(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
data, date := NewStockDataApi().GetStockMinutePriceData("usTSLA.OQ")
|
||||
logger.SugaredLogger.Infof("date:%s", date)
|
||||
logger.SugaredLogger.Infof("%+#v", *data)
|
||||
}
|
||||
func TestGetKLineData(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
k := NewStockDataApi().GetKLineData("sh600171", "240", 30)
|
||||
//for _, kline := range *k {
|
||||
// logger.SugaredLogger.Infof("%+#v", kline)
|
||||
//}
|
||||
jsonData, _ := json.Marshal(*k)
|
||||
markdownTable, err := JSONToMarkdownTable(jsonData)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
|
||||
|
||||
}
|
||||
func TestGetHK_KLineData(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
k := NewStockDataApi().GetHK_KLineData("hk01810", "day", 1)
|
||||
jsonData, _ := json.Marshal(*k)
|
||||
markdownTable, err := JSONToMarkdownTable(jsonData)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
|
||||
|
||||
}
|
||||
|
||||
func TestGetHKStockInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
//NewStockDataApi().GetHKStockInfo(200)
|
||||
//NewStockDataApi().GetSinaHKStockInfo()
|
||||
//m:105,m:106,m:107 //美股
|
||||
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
|
||||
//287 224 605
|
||||
for i := 1; i <= 605; i++ {
|
||||
NewStockDataApi().getDCStockInfo("us", i, 20)
|
||||
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTxStockData(t *testing.T) {
|
||||
str := "v_r_hk09660=\"100~地平线机器人-W~09660~6.340~5.690~5.800~210980204.0~0~0~6.340~0~0~0~0~0~0~0~0~0~6.340~0~0~0~0~0~0~0~0~0~210980204.0~2025/04/29\n14:14:52~0.650~11.42~6.450~5.710~6.340~210980204.0~1295585259.040~0~33.03~~0~0~13.01~702.2123~836.8986~HORIZONROBOT-W~0.00~10.380~3.320~1.00~-53.74~0~0~0~0~0~33.03~6.50~1.90~600~76.11~19.85~GP~19.70~11.51~0.63~-17.23~46.76~13200293682.00~11075904412.00~33.03~0.000~6.141~58.90~HKD~1~30\";"
|
||||
//str = "v_sz002241=\"51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~\n~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0\";"
|
||||
str = "v_sz002241=\"51~歌尔股份~002241~21.92~22.27~22.14~109872~40211~69642~21.91~25~21.90~961~21.89~257~21.88~748~21.87~665~21.92~86~21.93~168~21.94~556~21.95~171~21.96~85~~20250509094209~-0.35~-1.57~22.16~21.84~21.92/109872/241183171~109872~24118~0.36~27.78~~22.16~21.84~1.44~675.97~765.22~2.27~24.50~20.04~2.57~1590~21.95~40.80~28.71~~~1.24~24118.3171~0.0000~0~\n~GP-A~-15.07~5.13~1.11~8.18~3.39~30.63~15.70~5.23~15.67~-25.11~3083811231~3490989083~42.72~10.31~3083811231~~~37.23~0.18~~CNY~0~~21.85~1952\";"
|
||||
//str = "v_r_hk09660=\"100~地平线机器人-W~09660~6.860~7.000~7.010~21157200.0~0~0~6.860~0~0~0~0~0~0~0~0~0~6.860~0~0~0~0~0~0~0~0~0~21157200.0~2025/05/09\n09:43:13~-0.140~-2.00~7.030~6.730~6.860~21157200.0~144331073.000~0~35.74~~0~0~4.29~759.8070~905.5401~HORIZONROBOT-W~0.00~10.380~3.320~2.93~11.10~0~0~0~0~0~35.74~7.04~0.19~600~90.56~4.73~GP~19.70~11.51~17.26~48.48~13.58~13200293682.00~11075904412.00~35.74~0.000~6.822~71.93~HKD~1~30\";"
|
||||
info, _ := ParseTxStockData(str)
|
||||
logger.SugaredLogger.Infof("%+#v", info)
|
||||
}
|
||||
|
||||
func TestGetRealTimeStockPriceInfo(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
@@ -115,7 +184,7 @@ func TestParseFullSingleStockData(t *testing.T) {
|
||||
func TestNewStockDataApi(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla")
|
||||
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700")
|
||||
for _, data := range *datas {
|
||||
t.Log(data)
|
||||
}
|
||||
@@ -176,7 +245,7 @@ func TestReadFile(t *testing.T) {
|
||||
func TestFollowedList(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
stockDataApi.GetFollowList()
|
||||
stockDataApi.GetFollowList(1)
|
||||
|
||||
}
|
||||
|
||||
|
||||
50
backend/data/stock_data_api_windows.go
Normal file
50
backend/data/stock_data_api_windows.go
Normal file
@@ -0,0 +1,50 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package data
|
||||
|
||||
import "golang.org/x/sys/windows/registry"
|
||||
|
||||
// CheckChrome 在 Windows 系统上检查谷歌浏览器是否安装
|
||||
func CheckChrome() (string, bool) {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
defer key.Close()
|
||||
path, _, err := key.GetStringValue("Path")
|
||||
//logger.SugaredLogger.Infof("Chrome安装路径:%s", path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return path + "\\chrome.exe", true
|
||||
}
|
||||
|
||||
// CheckBrowser 在 Windows 系统上检查Edge浏览器是否安装,并返回安装路径
|
||||
func CheckBrowser() (string, bool) {
|
||||
if path, ok := CheckChrome(); ok {
|
||||
return path, true
|
||||
}
|
||||
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
defer key.Close()
|
||||
path, _, err := key.GetStringValue("Path")
|
||||
//logger.SugaredLogger.Infof("Edge安装路径:%s", path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return path + "\\msedge.exe", true
|
||||
}
|
||||
80
backend/data/stock_group_api.go
Normal file
80
backend/data/stock_group_api.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/4/3 11:18
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type Group struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"index"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
func (Group) TableName() string {
|
||||
return "stock_groups"
|
||||
}
|
||||
|
||||
type GroupStock struct {
|
||||
gorm.Model
|
||||
StockCode string `json:"stockCode" gorm:"index"`
|
||||
GroupId int `json:"groupId" gorm:"index"`
|
||||
GroupInfo Group `json:"groupInfo" gorm:"foreignKey:GroupId;references:ID"`
|
||||
}
|
||||
|
||||
func (GroupStock) TableName() string {
|
||||
return "group_stock_info"
|
||||
}
|
||||
|
||||
type StockGroupApi struct {
|
||||
dao *gorm.DB
|
||||
}
|
||||
|
||||
func NewStockGroupApi(dao *gorm.DB) *StockGroupApi {
|
||||
return &StockGroupApi{dao: db.Dao}
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) AddGroup(group Group) bool {
|
||||
err := receiver.dao.Where("name = ?", group.Name).FirstOrCreate(&group).Updates(&Group{
|
||||
Name: group.Name,
|
||||
Sort: group.Sort,
|
||||
}).Error
|
||||
return err == nil
|
||||
}
|
||||
func (receiver StockGroupApi) GetGroupList() []Group {
|
||||
var groups []Group
|
||||
receiver.dao.Find(&groups)
|
||||
return groups
|
||||
}
|
||||
func (receiver StockGroupApi) GetGroupStockByGroupId(groupId int) []GroupStock {
|
||||
var stockGroup []GroupStock
|
||||
receiver.dao.Preload("GroupInfo").Where("group_id = ?", groupId).Find(&stockGroup)
|
||||
return stockGroup
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) AddStockGroup(groupId int, stockCode string) bool {
|
||||
err := receiver.dao.Where("group_id = ? and stock_code = ?", groupId, stockCode).FirstOrCreate(&GroupStock{
|
||||
GroupId: groupId,
|
||||
StockCode: stockCode,
|
||||
}).Updates(&GroupStock{
|
||||
GroupId: groupId,
|
||||
StockCode: stockCode,
|
||||
}).Error
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) RemoveStockGroup(code string, name string, id int) bool {
|
||||
err := receiver.dao.Where("group_id = ? and stock_code = ?", id, code).Delete(&GroupStock{}).Error
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) RemoveGroup(id int) bool {
|
||||
err := receiver.dao.Where("id = ?", id).Delete(&Group{}).Error
|
||||
err = receiver.dao.Where("group_id = ?", id).Delete(&GroupStock{}).Error
|
||||
return err == nil
|
||||
|
||||
}
|
||||
290
backend/data/stock_sentiment_analysis.go
Normal file
290
backend/data/stock_sentiment_analysis.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/go-ego/gse"
|
||||
"go-stock/backend/logger"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 金融情感词典,包含股票市场相关的专业词汇
|
||||
var (
|
||||
seg gse.Segmenter
|
||||
|
||||
// 正面金融词汇及其权重
|
||||
positiveFinanceWords = map[string]float64{
|
||||
"上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5,
|
||||
"利好": 2.5, "增持": 2.0, "买入": 2.0, "推荐": 1.5, "看多": 2.0,
|
||||
"盈利": 2.0, "增长": 2.0, "超预期": 2.5, "强劲": 1.5, "回升": 1.5,
|
||||
"复苏": 2.0, "突破": 2.0, "创新高": 3.0, "回暖": 1.5, "上扬": 1.5,
|
||||
"利好消息": 3.0, "收益增长": 2.5, "利润增长": 2.5, "业绩优异": 2.5,
|
||||
"潜力股": 2.0, "绩优股": 2.0, "强势": 1.5, "走高": 1.5, "攀升": 1.5,
|
||||
"大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "爆发": 2.5, "暴涨": 3.0,
|
||||
}
|
||||
|
||||
// 负面金融词汇及其权重
|
||||
negativeFinanceWords = map[string]float64{
|
||||
"下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 1.5, "新低": 2.5,
|
||||
"利空": 2.5, "减持": 2.0, "卖出": 2.0, "看空": 2.0, "亏损": 2.5,
|
||||
"下滑": 2.0, "萎缩": 2.0, "不及预期": 2.5, "疲软": 1.5, "恶化": 2.0,
|
||||
"衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 1.5, "下挫": 1.5,
|
||||
"利空消息": 3.0, "收益下降": 2.5, "利润下滑": 2.5, "业绩不佳": 2.5,
|
||||
"垃圾股": 2.0, "风险股": 2.0, "弱势": 1.5, "走低": 1.5, "缩量": 2.5,
|
||||
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0,
|
||||
}
|
||||
|
||||
// 否定词,用于反转情感极性
|
||||
negationWords = map[string]struct{}{
|
||||
"不": {}, "没": {}, "无": {}, "非": {}, "未": {}, "别": {}, "勿": {},
|
||||
}
|
||||
|
||||
// 程度副词,用于调整情感强度
|
||||
degreeWords = map[string]float64{
|
||||
"非常": 1.8, "极其": 2.2, "太": 1.8, "很": 1.5,
|
||||
"比较": 0.8, "稍微": 0.6, "有点": 0.7, "显著": 1.5,
|
||||
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7,
|
||||
}
|
||||
|
||||
// 转折词,用于识别情感转折
|
||||
transitionWords = map[string]struct{}{
|
||||
"但是": {}, "然而": {}, "不过": {}, "却": {}, "可是": {},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 加载默认词典
|
||||
err := seg.LoadDict()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// SentimentResult 情感分析结果类型
|
||||
type SentimentResult struct {
|
||||
Score float64 // 情感得分
|
||||
Category SentimentType // 情感类别
|
||||
PositiveCount int // 正面词数量
|
||||
NegativeCount int // 负面词数量
|
||||
Description string // 情感描述
|
||||
}
|
||||
|
||||
// SentimentType 情感类型枚举
|
||||
type SentimentType int
|
||||
|
||||
const (
|
||||
Positive SentimentType = iota
|
||||
Negative
|
||||
Neutral
|
||||
)
|
||||
|
||||
// AnalyzeSentiment 判断文本的情感
|
||||
func AnalyzeSentiment(text string) SentimentResult {
|
||||
// 初始化得分
|
||||
score := 0.0
|
||||
positiveCount := 0
|
||||
negativeCount := 0
|
||||
|
||||
// 分词(简单按单个字符分割)
|
||||
words := splitWords(text)
|
||||
|
||||
// 检查文本是否包含转折词,并分割成两部分
|
||||
var transitionIndex int
|
||||
var hasTransition bool
|
||||
for i, word := range words {
|
||||
if _, ok := transitionWords[word]; ok {
|
||||
transitionIndex = i
|
||||
hasTransition = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 处理有转折的文本
|
||||
if hasTransition {
|
||||
// 转折前的部分
|
||||
preTransitionWords := words[:transitionIndex]
|
||||
preScore, prePos, preNeg := calculateScore(preTransitionWords)
|
||||
|
||||
// 转折后的部分,权重加倍
|
||||
postTransitionWords := words[transitionIndex+1:]
|
||||
postScore, postPos, postNeg := calculateScore(postTransitionWords)
|
||||
postScore *= 1.5 // 转折后的情感更重要
|
||||
|
||||
score = preScore + postScore
|
||||
positiveCount = prePos + postPos
|
||||
negativeCount = preNeg + postNeg
|
||||
} else {
|
||||
// 没有转折的文本
|
||||
score, positiveCount, negativeCount = calculateScore(words)
|
||||
}
|
||||
|
||||
// 确定情感类别
|
||||
var category SentimentType
|
||||
switch {
|
||||
case score > 1.0:
|
||||
category = Positive
|
||||
case score < -1.0:
|
||||
category = Negative
|
||||
default:
|
||||
category = Neutral
|
||||
}
|
||||
|
||||
return SentimentResult{
|
||||
Score: score,
|
||||
Category: category,
|
||||
PositiveCount: positiveCount,
|
||||
NegativeCount: negativeCount,
|
||||
Description: GetSentimentDescription(category),
|
||||
}
|
||||
}
|
||||
|
||||
// 计算情感得分
|
||||
func calculateScore(words []string) (float64, int, int) {
|
||||
score := 0.0
|
||||
positiveCount := 0
|
||||
negativeCount := 0
|
||||
|
||||
// 遍历每个词,计算情感得分
|
||||
for i, word := range words {
|
||||
// 首先检查是否为程度副词
|
||||
degree, isDegree := degreeWords[word]
|
||||
|
||||
// 检查是否为否定词
|
||||
_, isNegation := negationWords[word]
|
||||
|
||||
// 检查是否为金融正面词
|
||||
if posScore, isPositive := positiveFinanceWords[word]; isPositive {
|
||||
// 检查前一个词是否为否定词或程度副词
|
||||
if i > 0 {
|
||||
prevWord := words[i-1]
|
||||
if _, isNeg := negationWords[prevWord]; isNeg {
|
||||
score -= posScore
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if deg, isDeg := degreeWords[prevWord]; isDeg {
|
||||
score += posScore * deg
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
score += posScore
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否为金融负面词
|
||||
if negScore, isNegative := negativeFinanceWords[word]; isNegative {
|
||||
// 检查前一个词是否为否定词或程度副词
|
||||
if i > 0 {
|
||||
prevWord := words[i-1]
|
||||
if _, isNeg := negationWords[prevWord]; isNeg {
|
||||
score += negScore
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if deg, isDeg := degreeWords[prevWord]; isDeg {
|
||||
score -= negScore * deg
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
score -= negScore
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理程度副词(如果后面跟着情感词)
|
||||
if isDegree && i+1 < len(words) {
|
||||
nextWord := words[i+1]
|
||||
|
||||
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
|
||||
score += posScore * degree
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
|
||||
score -= negScore * degree
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 处理否定词(如果后面跟着情感词)
|
||||
if isNegation && i+1 < len(words) {
|
||||
nextWord := words[i+1]
|
||||
|
||||
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
|
||||
score -= posScore
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
|
||||
score += negScore
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score, positiveCount, negativeCount
|
||||
}
|
||||
|
||||
// 简单的分词函数,考虑了中文和英文
|
||||
func splitWords(text string) []string {
|
||||
return seg.Cut(text, true)
|
||||
}
|
||||
|
||||
// GetSentimentDescription 获取情感类别的文本描述
|
||||
func GetSentimentDescription(category SentimentType) string {
|
||||
switch category {
|
||||
case Positive:
|
||||
return "看涨"
|
||||
case Negative:
|
||||
return "看跌"
|
||||
case Neutral:
|
||||
return "中性"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 从命令行读取输入
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Println("请输入要分析的股市相关文本(输入exit退出):")
|
||||
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
text, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Println("读取输入时出错:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 去除换行符
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
// 检查是否退出
|
||||
if text == "exit" {
|
||||
break
|
||||
}
|
||||
|
||||
// 分析情感
|
||||
result := AnalyzeSentiment(text)
|
||||
|
||||
// 输出结果
|
||||
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
|
||||
GetSentimentDescription(result.Category),
|
||||
result.Score,
|
||||
result.PositiveCount,
|
||||
result.NegativeCount)
|
||||
}
|
||||
}
|
||||
36
backend/data/stock_sentiment_analysis_test.go
Normal file
36
backend/data/stock_sentiment_analysis_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/6/19 13:05
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestAnalyzeSentiment(t *testing.T) {
|
||||
// 分析情感
|
||||
text := " 【调查:韩国近两成中小学生过度使用智能手机或互联网】财联社6月19日电,韩国女性家族部18日公布的一项年度调查结果显示,接受调查的韩国中小学生中,共计约17.3%、即超过21万人使用智能手机或互联网的程度达到了“危险水平”,这意味着他们因过度依赖智能手机或互联网而需要关注或干预,这一比例引人担忧。 (新华社)\n"
|
||||
text = "消息人士称,联合利华(Unilever)正在为Graze零食品牌寻找买家。\n"
|
||||
text = "【韩国未来5年将投入51万亿韩元发展文化产业】 据韩联社,韩国文化体育观光部(文体部)今后5年将投入51万亿韩元(约合人民币2667亿元)预算,落实总统李在明在竞选时期提出的“将韩国打造成全球五大文化强国之一”的承诺。\n"
|
||||
//text = "【油气股持续拉升 国际实业午后涨停】财联社6月19日电,油气股午后持续拉升,国际实业、宝莫股份午后涨停,准油股份、山东墨龙。茂化实华此前涨停,通源石油、海默科技、贝肯能源、中曼石油、科力股份等多股涨超5%。\n"
|
||||
//text = " 【三大指数均跌逾1% 下跌个股近4800只】财联社6月19日电,指数持续走弱,沪指下挫跌逾1.00%,深成指跌1.25%,创业板指跌1.39%。核聚变、风电、军工、食品消费等板块指数跌幅居前,沪深京三市下跌个股近4800只。\n"
|
||||
text = "【银行理财首单网下打新落地】财联社6月20日电,记者从多渠道获悉,光大理财以申报价格17元参与信通电子网下打新,并成功入围有效报价,成为行业内首家参与网下打新的银行理财公司。光大理财工作人员向证券时报记者表示,本次光大理财是以其管理的混合类产品“阳光橙增盈绝对收益策略”参与了此次网下打新,该产品为光大理财“固收+”银行理财产品。资料显示,信通电子成立于1996年,核心产品包括输电线路智能巡检系统、变电站智能辅控系统、移动智能终端及其他产品。根据其招股说明书,信通电子2023、2024年营业收入分别较上年增长19.08%和7.97%,净利润分别较上年增长5.6%和15.11%。 (证券时报)"
|
||||
text = " 【以军称拦截数枚伊朗导弹】财联社6月20日电,据央视新闻报道,以军在贝尔谢巴及周边区域拦截了数枚伊朗导弹,但仍有导弹或拦截残骸落地。以色列国防军发文表示,搜救队伍正在一处“空中物体落地”的所在区域开展工作,公众目前可以离开避难场所。伊朗方面对上述说法暂无回应。"
|
||||
|
||||
words := splitWords(text)
|
||||
fmt.Println(strings.Join(words, " "))
|
||||
|
||||
result := AnalyzeSentiment(text)
|
||||
|
||||
// 输出结果
|
||||
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
|
||||
result.Description,
|
||||
result.Score,
|
||||
result.PositiveCount,
|
||||
result.NegativeCount)
|
||||
|
||||
}
|
||||
@@ -64,7 +64,13 @@ func GetTableMarkdown(document *goquery.Document, waitVisible string, markdown *
|
||||
document.Find(waitVisible).First().Find("tr").Each(func(index int, item *goquery.Selection) {
|
||||
row := ""
|
||||
item.Find("th, td").Each(func(i int, cell *goquery.Selection) {
|
||||
text := cell.Text()
|
||||
text := cell.Children().FilterFunction(func(i int, s *goquery.Selection) bool {
|
||||
return isVisible(s)
|
||||
}).Text()
|
||||
if text == "" {
|
||||
text = cell.Text()
|
||||
}
|
||||
|
||||
row += "|" + text
|
||||
})
|
||||
row += "|"
|
||||
@@ -85,3 +91,21 @@ func GetTableMarkdown(document *goquery.Document, waitVisible string, markdown *
|
||||
})
|
||||
logger.SugaredLogger.Infof("\n%s", markdown.String())
|
||||
}
|
||||
|
||||
// isVisible 函数用于判断元素是否可见
|
||||
func isVisible(s *goquery.Selection) bool {
|
||||
// 检查 display 属性
|
||||
display, _ := s.Attr("style")
|
||||
if strings.Contains(strings.ToLower(display), "display: none") {
|
||||
return false
|
||||
}
|
||||
// 检查 visibility 属性
|
||||
if strings.Contains(strings.ToLower(display), "visibility: hidden") {
|
||||
return false
|
||||
}
|
||||
// 检查 opacity 属性
|
||||
if strings.Contains(strings.ToLower(display), "opacity: 0") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func Init(sqlitePath string) {
|
||||
Colorful: false,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
ParameterizedQueries: false,
|
||||
LogLevel: logger.Warn,
|
||||
LogLevel: logger.Info,
|
||||
},
|
||||
)
|
||||
var openDb *gorm.DB
|
||||
|
||||
@@ -150,13 +150,15 @@ func (receiver AIResponseResult) TableName() string {
|
||||
|
||||
type VersionInfo struct {
|
||||
gorm.Model
|
||||
Version string `json:"version"`
|
||||
Content string `json:"content"`
|
||||
Icon string `json:"icon"`
|
||||
Alipay string `json:"alipay"`
|
||||
Wxpay string `json:"wxpay"`
|
||||
BuildTimeStamp int64 `json:"buildTimeStamp"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
Version string `json:"version"`
|
||||
Content string `json:"content"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (receiver VersionInfo) TableName() string {
|
||||
@@ -170,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 {
|
||||
@@ -185,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 {
|
||||
@@ -194,6 +200,12 @@ func (receiver StockInfoUS) TableName() string {
|
||||
type Resp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param string `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type PromptTemplate struct {
|
||||
@@ -215,3 +227,241 @@ type Prompt struct {
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Telegraph struct {
|
||||
gorm.Model
|
||||
Time string `json:"time"`
|
||||
Content string `json:"content"`
|
||||
SubjectTags []string `json:"subjects" gorm:"-:all"`
|
||||
StocksTags []string `json:"stocks" gorm:"-:all"`
|
||||
IsRed bool `json:"isRed"`
|
||||
Url string `json:"url"`
|
||||
Source string `json:"source"`
|
||||
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
|
||||
SentimentResult string `json:"sentimentResult" gorm:"-:all"`
|
||||
}
|
||||
type TelegraphTags struct {
|
||||
gorm.Model
|
||||
TagId uint `json:"tagId"`
|
||||
TelegraphId uint `json:"telegraphId"`
|
||||
}
|
||||
|
||||
func (t TelegraphTags) TableName() string {
|
||||
return "telegraph_tags"
|
||||
}
|
||||
|
||||
type Tags struct {
|
||||
gorm.Model
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (p Tags) TableName() string {
|
||||
return "tags"
|
||||
}
|
||||
|
||||
func (p Telegraph) TableName() string {
|
||||
return "telegraph_list"
|
||||
}
|
||||
|
||||
type SinaStockInfo struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
Engname string `json:"engname"`
|
||||
Tradetype string `json:"tradetype"`
|
||||
Lasttrade string `json:"lasttrade"`
|
||||
Prevclose string `json:"prevclose"`
|
||||
Open string `json:"open"`
|
||||
High string `json:"high"`
|
||||
Low string `json:"low"`
|
||||
Volume string `json:"volume"`
|
||||
Currentvolume string `json:"currentvolume"`
|
||||
Amount string `json:"amount"`
|
||||
Ticktime string `json:"ticktime"`
|
||||
Buy string `json:"buy"`
|
||||
Sell string `json:"sell"`
|
||||
High52Week string `json:"high_52week"`
|
||||
Low52Week string `json:"low_52week"`
|
||||
Eps string `json:"eps"`
|
||||
Dividend string `json:"dividend"`
|
||||
StocksSum string `json:"stocks_sum"`
|
||||
Pricechange string `json:"pricechange"`
|
||||
Changepercent string `json:"changepercent"`
|
||||
MarketValue string `json:"market_value"`
|
||||
PeRatio string `json:"pe_ratio"`
|
||||
}
|
||||
|
||||
type LongTigerRankData struct {
|
||||
ACCUMAMOUNT float64 `json:"ACCUM_AMOUNT"`
|
||||
BILLBOARDBUYAMT float64 `json:"BILLBOARD_BUY_AMT"`
|
||||
BILLBOARDDEALAMT float64 `json:"BILLBOARD_DEAL_AMT"`
|
||||
BILLBOARDNETAMT float64 `json:"BILLBOARD_NET_AMT"`
|
||||
BILLBOARDSELLAMT float64 `json:"BILLBOARD_SELL_AMT"`
|
||||
CHANGERATE float64 `json:"CHANGE_RATE"`
|
||||
CLOSEPRICE float64 `json:"CLOSE_PRICE"`
|
||||
DEALAMOUNTRATIO float64 `json:"DEAL_AMOUNT_RATIO"`
|
||||
DEALNETRATIO float64 `json:"DEAL_NET_RATIO"`
|
||||
EXPLAIN string `json:"EXPLAIN"`
|
||||
EXPLANATION string `json:"EXPLANATION"`
|
||||
FREEMARKETCAP float64 `json:"FREE_MARKET_CAP"`
|
||||
SECUCODE string `json:"SECUCODE" gorm:"index"`
|
||||
SECURITYCODE string `json:"SECURITY_CODE"`
|
||||
SECURITYNAMEABBR string `json:"SECURITY_NAME_ABBR"`
|
||||
SECURITYTYPECODE string `json:"SECURITY_TYPE_CODE"`
|
||||
TRADEDATE string `json:"TRADE_DATE" gorm:"index"`
|
||||
TURNOVERRATE float64 `json:"TURNOVERRATE"`
|
||||
}
|
||||
|
||||
func (l LongTigerRankData) TableName() string {
|
||||
return "long_tiger_rank"
|
||||
}
|
||||
|
||||
type TVNews struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Published int `json:"published"`
|
||||
Urgency int `json:"urgency"`
|
||||
Permission string `json:"permission"`
|
||||
StoryPath string `json:"storyPath"`
|
||||
Provider struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
LogoId string `json:"logo_id"`
|
||||
} `json:"provider"`
|
||||
}
|
||||
|
||||
type XUEQIUHot struct {
|
||||
Data struct {
|
||||
Items []HotItem `json:"items"`
|
||||
ItemsSize int `json:"items_size"`
|
||||
} `json:"data"`
|
||||
ErrorCode int `json:"error_code"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
type HotItem struct {
|
||||
Type int `json:"type"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Value float64 `json:"value"`
|
||||
Increment int `json:"increment"`
|
||||
RankChange int `json:"rank_change"`
|
||||
HasExist interface{} `json:"has_exist"`
|
||||
Symbol string `json:"symbol"`
|
||||
Percent float64 `json:"percent"`
|
||||
Current float64 `json:"current"`
|
||||
Chg float64 `json:"chg"`
|
||||
Exchange string `json:"exchange"`
|
||||
StockType int `json:"stock_type"`
|
||||
SubType string `json:"sub_type"`
|
||||
Ad int `json:"ad"`
|
||||
AdId interface{} `json:"ad_id"`
|
||||
ContentId interface{} `json:"content_id"`
|
||||
Page interface{} `json:"page"`
|
||||
Model interface{} `json:"model"`
|
||||
Location interface{} `json:"location"`
|
||||
TradeSession interface{} `json:"trade_session"`
|
||||
CurrentExt interface{} `json:"current_ext"`
|
||||
PercentExt interface{} `json:"percent_ext"`
|
||||
}
|
||||
|
||||
type HotEvent struct {
|
||||
PicSize interface{} `json:"pic_size"`
|
||||
Tag string `json:"tag"`
|
||||
Id int `json:"id"`
|
||||
Pic string `json:"pic"`
|
||||
Hot int `json:"hot"`
|
||||
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"`
|
||||
}
|
||||
|
||||
221
backend/util/html_to_markdown.go
Normal file
221
backend/util/html_to_markdown.go
Normal 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("", 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
|
||||
}
|
||||
}
|
||||
6
backend/util/html_to_markdown_test.go
Normal file
6
backend/util/html_to_markdown_test.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package util
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/7/15 14:08
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
284
backend/util/struct_to_markdown.go
Normal file
284
backend/util/struct_to_markdown.go
Normal 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))
|
||||
}
|
||||
54
backend/util/struct_to_markdown_test.go
Normal file
54
backend/util/struct_to_markdown_test.go
Normal 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))
|
||||
}
|
||||
BIN
build/screenshot/img13.png
Normal file
BIN
build/screenshot/img13.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 336 KiB |
BIN
build/screenshot/img_12.png
Normal file
BIN
build/screenshot/img_12.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
BIN
build/screenshot/img_13.png
Normal file
BIN
build/screenshot/img_13.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
BIN
build/screenshot/img_14.png
Normal file
BIN
build/screenshot/img_14.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
709
frontend/package-lock.json
generated
709
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,8 @@
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@vavt/cm-extension": "^1.8.0",
|
||||
"@vavt/v3-extension": "^3.0.0",
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"echarts": "^5.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -22,11 +23,19 @@
|
||||
"vue3-danmaku": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vicons/antd": "^0.13.0",
|
||||
"@vicons/carbon": "^0.13.0",
|
||||
"@vicons/fa": "^0.13.0",
|
||||
"@vicons/fluent": "^0.13.0",
|
||||
"@vicons/ionicons4": "^0.13.0",
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vicons/tabler": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"html-docx-js-typescript": "^0.1.5",
|
||||
"naive-ui": "^2.41.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vite": "^5.4.12"
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"keywords": [
|
||||
"AI赋能股票分析",
|
||||
|
||||
@@ -1 +1 @@
|
||||
cf858d682535e094e087a036e009d7f8
|
||||
2d63c3a999d797889c01d6c96451b197
|
||||
@@ -1,32 +1,56 @@
|
||||
<script setup>
|
||||
import {
|
||||
EventsEmit,
|
||||
EventsOff,
|
||||
EventsOn,
|
||||
Quit,
|
||||
WindowFullscreen, WindowGetPosition,
|
||||
WindowFullscreen,
|
||||
WindowHide,
|
||||
WindowSetPosition,
|
||||
WindowUnfullscreen
|
||||
} from '../wailsjs/runtime'
|
||||
import {h, onBeforeMount, onMounted, ref} from "vue";
|
||||
import { RouterLink } from 'vue-router'
|
||||
import {darkTheme, NGradientText, NIcon, NText,} from 'naive-ui'
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {RouterLink, useRouter} from 'vue-router'
|
||||
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,NButton,dateZhCN,zhCN} from 'naive-ui'
|
||||
import {
|
||||
SettingsOutline,
|
||||
AlarmOutline,
|
||||
AnalyticsOutline,
|
||||
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
|
||||
ExpandOutline, Flag,
|
||||
Flame, FlameSharp, InformationOutline,
|
||||
LogoGithub,
|
||||
NewspaperOutline,
|
||||
NewspaperSharp, Notifications,
|
||||
PowerOutline, Pulse,
|
||||
ReorderTwoOutline,
|
||||
ExpandOutline,
|
||||
PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, AlarmOutline, SparklesOutline,
|
||||
SettingsOutline, Skull, SkullOutline, SkullSharp,
|
||||
SparklesOutline,
|
||||
StarOutline,
|
||||
Wallet, WarningOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import {GetConfig} from "../wailsjs/go/main/App";
|
||||
const enableNews= ref(false)
|
||||
const contentStyle = ref("")
|
||||
const enableDarkTheme = ref(true)
|
||||
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
import {AnalyzeSentiment, GetConfig, GetGroupList,GetVersionInfo} from "../wailsjs/go/main/App";
|
||||
import {Dragon, Fire, Gripfire} from "@vicons/fa";
|
||||
import {ReportSearch} from "@vicons/tabler";
|
||||
import {LocalFireDepartmentRound} from "@vicons/material";
|
||||
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
|
||||
import {FireFilled, FireOutlined, NotificationFilled, StockOutlined} from "@vicons/antd";
|
||||
|
||||
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const loadingMsg = ref("加载数据中...")
|
||||
const enableNews = ref(false)
|
||||
const contentStyle = ref("")
|
||||
const enableFund = ref(false)
|
||||
const enableDarkTheme = ref(null)
|
||||
const content = ref('未经授权,禁止商业目的!\n\n数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
const isFullscreen = ref(false)
|
||||
const activeKey = ref('')
|
||||
const containerRef= ref({})
|
||||
const realtimeProfit= ref(0)
|
||||
const telegraph= ref([])
|
||||
const activeKey = ref('stock')
|
||||
const containerRef = ref({})
|
||||
const realtimeProfit = ref(0)
|
||||
const telegraph = ref([])
|
||||
const groupList = ref([])
|
||||
const menuOptions = ref([
|
||||
{
|
||||
label: () =>
|
||||
@@ -35,20 +59,315 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'stock',
|
||||
params: {
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
}
|
||||
params: {},
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'stock'
|
||||
},
|
||||
},
|
||||
{ default: () => '股票自选',}
|
||||
{default: () => '股票自选',}
|
||||
),
|
||||
key: 'stock',
|
||||
icon: renderIcon(StarOutline),
|
||||
children:[
|
||||
children: [
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(WalletOutline),
|
||||
label: () =>
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
href: '#',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
activeKey.value = 'stock'
|
||||
//console.log("push",item)
|
||||
router.push({
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
})
|
||||
EventsEmit("changeTab", {ID: 0, name: '全部'})
|
||||
},
|
||||
to: {
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
{default: () => '全部',}
|
||||
),
|
||||
key: 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
params: {}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
|
||||
},
|
||||
},
|
||||
{default: () => '市场行情'}
|
||||
),
|
||||
key: 'market',
|
||||
icon: renderIcon(NewspaperOutline),
|
||||
children: [
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "市场快讯",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
|
||||
},
|
||||
},
|
||||
{default: () => '市场快讯',}
|
||||
),
|
||||
key: 'market1',
|
||||
icon: renderIcon(NewspaperSharp),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "全球股指",
|
||||
},
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
|
||||
},
|
||||
},
|
||||
{default: () => '全球股指',}
|
||||
),
|
||||
key: 'market2',
|
||||
icon: renderIcon(BarChartSharp),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "重大指数",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '重大指数'})
|
||||
},
|
||||
},
|
||||
{default: () => '重大指数',}
|
||||
),
|
||||
key: 'market3',
|
||||
icon: renderIcon(AnalyticsOutline),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "行业排名",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
|
||||
},
|
||||
},
|
||||
{default: () => '行业排名',}
|
||||
),
|
||||
key: 'market4',
|
||||
icon: renderIcon(Flag),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "个股资金流向",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
|
||||
},
|
||||
},
|
||||
{default: () => '个股资金流向',}
|
||||
),
|
||||
key: 'market5',
|
||||
icon: renderIcon(Pulse),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "龙虎榜",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
|
||||
},
|
||||
},
|
||||
{default: () => '龙虎榜',}
|
||||
),
|
||||
key: 'market6',
|
||||
icon: renderIcon(Dragon),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "个股研报",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
|
||||
},
|
||||
},
|
||||
{default: () => '个股研报',}
|
||||
),
|
||||
key: 'market7',
|
||||
icon: renderIcon(StockOutlined),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "公司公告",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
|
||||
},
|
||||
},
|
||||
{default: () => '公司公告',}
|
||||
),
|
||||
key: 'market8',
|
||||
icon: renderIcon(NotificationFilled),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "行业研究",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
|
||||
},
|
||||
},
|
||||
{default: () => '行业研究',}
|
||||
),
|
||||
key: 'market9',
|
||||
icon: renderIcon(ReportSearch),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "当前热门",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
|
||||
},
|
||||
},
|
||||
{default: () => '当前热门',}
|
||||
),
|
||||
key: 'market10',
|
||||
icon: renderIcon(Gripfire),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "指标选股",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
|
||||
},
|
||||
},
|
||||
{default: () => '指标选股',}
|
||||
),
|
||||
key: 'market11',
|
||||
icon: renderIcon(BoxSearch20Regular),
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -59,17 +378,22 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'fund',
|
||||
params: {
|
||||
query: {
|
||||
name: '基金自选',
|
||||
},
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'fund'
|
||||
},
|
||||
},
|
||||
{ default: () => '基金自选',}
|
||||
{default: () => '基金自选',}
|
||||
),
|
||||
show: enableFund.value,
|
||||
key: 'fund',
|
||||
icon: renderIcon(SparklesOutline),
|
||||
children:[
|
||||
children: [
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '功能完善中!'}),
|
||||
label: () => h(NText, {type: realtimeProfit.value > 0 ? 'error' : 'success'}, {default: () => '功能完善中!'}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(AlarmOutline),
|
||||
@@ -83,11 +407,15 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'settings',
|
||||
params: {
|
||||
}
|
||||
query: {
|
||||
name:"设置",
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'settings'
|
||||
},
|
||||
}
|
||||
},
|
||||
{ default: () => '设置' }
|
||||
{default: () => '设置'}
|
||||
),
|
||||
key: 'settings',
|
||||
icon: renderIcon(SettingsOutline),
|
||||
@@ -99,30 +427,34 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'about',
|
||||
params: {
|
||||
query: {
|
||||
name:"关于",
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'about'
|
||||
},
|
||||
},
|
||||
{ default: () => '关于' }
|
||||
{default: () => '关于'}
|
||||
),
|
||||
key: 'about',
|
||||
icon: renderIcon(LogoGithub),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: toggleFullscreen,
|
||||
title: '全屏 Ctrl+F 退出全屏 Esc',
|
||||
}, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
|
||||
}, {default: () => isFullscreen.value ? '取消全屏' : '全屏'}),
|
||||
key: 'full',
|
||||
icon: renderIcon(ExpandOutline),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: WindowHide,
|
||||
title: '隐藏到托盘区 Ctrl+H',
|
||||
}, { default: () => '隐藏到托盘区' }),
|
||||
title: '隐藏到托盘区 Ctrl+Z',
|
||||
}, {default: () => '隐藏到托盘区'}),
|
||||
key: 'hide',
|
||||
icon: renderIcon(ReorderTwoOutline),
|
||||
},
|
||||
@@ -136,28 +468,32 @@ const menuOptions = ref([
|
||||
// icon: renderIcon(MoveOutline),
|
||||
// },
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: Quit,
|
||||
}, { default: () => '退出程序' }),
|
||||
}, {default: () => '退出程序'}),
|
||||
key: 'exit',
|
||||
icon: renderIcon(PowerOutline),
|
||||
},
|
||||
])
|
||||
|
||||
function renderIcon(icon) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) })
|
||||
return () => h(NIcon, null, {default: () => h(icon)})
|
||||
}
|
||||
|
||||
function toggleFullscreen(e) {
|
||||
activeKey.value = 'full'
|
||||
//console.log(e)
|
||||
if (isFullscreen.value) {
|
||||
WindowUnfullscreen()
|
||||
//e.target.innerHTML = '全屏'
|
||||
} else {
|
||||
WindowFullscreen()
|
||||
// e.target.innerHTML = '取消全屏'
|
||||
}
|
||||
isFullscreen.value=!isFullscreen.value
|
||||
if (isFullscreen.value) {
|
||||
WindowUnfullscreen()
|
||||
//e.target.innerHTML = '全屏'
|
||||
} else {
|
||||
WindowFullscreen()
|
||||
// e.target.innerHTML = '取消全屏'
|
||||
}
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
// const drag = ref(false)
|
||||
// const lastPos= ref({x:0,y:0})
|
||||
// function toggleStartMoveWindow(e) {
|
||||
@@ -175,11 +511,29 @@ function toggleFullscreen(e) {
|
||||
// }
|
||||
// window.addEventListener('mousemove', dragstart)
|
||||
|
||||
EventsOn("realtime_profit",(data)=>{
|
||||
realtimeProfit.value=data
|
||||
EventsOn("realtime_profit", (data) => {
|
||||
realtimeProfit.value = data
|
||||
})
|
||||
EventsOn("telegraph",(data)=>{
|
||||
telegraph.value=data
|
||||
EventsOn("telegraph", (data) => {
|
||||
telegraph.value = data
|
||||
})
|
||||
|
||||
EventsOn("loadingMsg", (data) => {
|
||||
if(data==="done"){
|
||||
loadingMsg.value = "加载完成..."
|
||||
EventsEmit("loadingDone", "app")
|
||||
loading.value = false
|
||||
}else{
|
||||
loading.value = true
|
||||
loadingMsg.value = data
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("realtime_profit")
|
||||
EventsOff("loadingMsg")
|
||||
EventsOff("telegraph")
|
||||
EventsOff("newsPush")
|
||||
})
|
||||
|
||||
window.onerror = function (msg, source, lineno, colno, error) {
|
||||
@@ -195,89 +549,173 @@ window.onerror = function (msg, source, lineno, colno, error) {
|
||||
return true;
|
||||
};
|
||||
|
||||
onBeforeMount(()=>{
|
||||
// GetConfig().then((res)=>{
|
||||
// console.log(res)
|
||||
// if(res.darkTheme){
|
||||
// enableDarkTheme.value=darkTheme
|
||||
// }
|
||||
// if(res.enableNews){
|
||||
// enableNews.value=true
|
||||
// }
|
||||
// })
|
||||
})
|
||||
|
||||
onMounted(()=>{
|
||||
contentStyle.value="max-height: calc(90vh);overflow: hidden"
|
||||
GetConfig().then((res)=>{
|
||||
console.log(res)
|
||||
if(res.darkTheme){
|
||||
enableDarkTheme.value=darkTheme
|
||||
}else{
|
||||
enableDarkTheme.value=null
|
||||
onBeforeMount(() => {
|
||||
GetVersionInfo().then(result => {
|
||||
if(result.officialStatement){
|
||||
content.value = result.officialStatement+"\n\n"+content.value
|
||||
}
|
||||
if(res.enableNews){
|
||||
enableNews.value=true
|
||||
})
|
||||
|
||||
GetGroupList().then(result => {
|
||||
groupList.value = result
|
||||
menuOptions.value.map((item) => {
|
||||
//console.log(item)
|
||||
if (item.key === 'stock') {
|
||||
item.children.push(...groupList.value.map(item => {
|
||||
return {
|
||||
label: () =>
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
href: '#',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
//console.log("push",item)
|
||||
router.push({
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: item.name,
|
||||
groupId: item.ID,
|
||||
},
|
||||
})
|
||||
setTimeout(() => {
|
||||
EventsEmit("changeTab", item)
|
||||
}, 100)
|
||||
},
|
||||
to: {
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: item.name,
|
||||
groupId: item.ID,
|
||||
},
|
||||
}
|
||||
},
|
||||
{default: () => item.name,}
|
||||
),
|
||||
key: item.ID,
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
GetConfig().then((res) => {
|
||||
//console.log(res)
|
||||
enableFund.value = res.enableFund
|
||||
|
||||
menuOptions.value.filter((item) => {
|
||||
if (item.key === 'fund') {
|
||||
item.show = res.enableFund
|
||||
}
|
||||
})
|
||||
|
||||
if (res.darkTheme) {
|
||||
enableDarkTheme.value = darkTheme
|
||||
} else {
|
||||
enableDarkTheme.value = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
contentStyle.value = "max-height: calc(92vh);overflow: hidden"
|
||||
GetConfig().then((res) => {
|
||||
if (res.enableNews) {
|
||||
enableNews.value = true
|
||||
}
|
||||
enableFund.value = res.enableFund
|
||||
const {notification } =createDiscreteApi(["notification"], {
|
||||
configProviderProps: {
|
||||
theme: enableDarkTheme.value ? darkTheme : lightTheme ,
|
||||
max: 3,
|
||||
},
|
||||
})
|
||||
EventsOn("newsPush", (data) => {
|
||||
//console.log(data)
|
||||
if(data.isRed){
|
||||
notification.create({
|
||||
//type:"error",
|
||||
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
|
||||
title: data.time,
|
||||
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({
|
||||
//type:"info",
|
||||
//avatar: () => h(NIcon,{component:Notifications}),
|
||||
title: data.time,
|
||||
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 ,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<n-config-provider ref="containerRef" :theme="enableDarkTheme" >
|
||||
<n-message-provider >
|
||||
<n-config-provider ref="containerRef" :theme="enableDarkTheme" :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-message-provider>
|
||||
<n-notification-provider>
|
||||
<n-modal-provider>
|
||||
<n-dialog-provider>
|
||||
|
||||
<n-watermark
|
||||
:content="content"
|
||||
cross
|
||||
selectable
|
||||
:font-size="16"
|
||||
:line-height="16"
|
||||
:width="500"
|
||||
:height="400"
|
||||
:x-offset="50"
|
||||
:y-offset="150"
|
||||
:rotate="-15"
|
||||
>
|
||||
<n-flex justify="center">
|
||||
<n-grid x-gap="12" :cols="1">
|
||||
<!--
|
||||
<n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
|
||||
|
||||
</n-gi>
|
||||
-->
|
||||
|
||||
<n-gi>
|
||||
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%" v-if="(telegraph.length>0)&&(enableNews)">
|
||||
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
|
||||
{{item}}
|
||||
</n-tag>
|
||||
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
|
||||
</n-marquee>
|
||||
<n-scrollbar :style="contentStyle">
|
||||
<RouterView />
|
||||
</n-scrollbar>
|
||||
</n-gi>
|
||||
|
||||
|
||||
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
|
||||
<n-card size="small" style="--wails-draggable:drag">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
responsive
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
</n-watermark>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
<n-modal-provider>
|
||||
<n-dialog-provider>
|
||||
<n-watermark
|
||||
:content="content"
|
||||
cross
|
||||
selectable
|
||||
:font-size="16"
|
||||
:line-height="16"
|
||||
:width="500"
|
||||
:height="400"
|
||||
:x-offset="50"
|
||||
:y-offset="150"
|
||||
:rotate="-15"
|
||||
>
|
||||
<n-flex>
|
||||
<n-grid x-gap="12" :cols="1">
|
||||
<n-gi>
|
||||
<n-spin :show="loading">
|
||||
<template #description>
|
||||
{{ loadingMsg }}
|
||||
</template>
|
||||
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%"
|
||||
v-if="(telegraph.length>0)&&(enableNews)">
|
||||
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
|
||||
{{ item }}
|
||||
</n-tag>
|
||||
</n-marquee>
|
||||
<n-scrollbar :style="contentStyle">
|
||||
<n-skeleton v-if="loading" height="calc(100vh)" />
|
||||
<RouterView/>
|
||||
</n-scrollbar>
|
||||
</n-spin>
|
||||
</n-gi>
|
||||
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
|
||||
<n-card size="small" style="--wails-draggable:drag">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
responsive
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
</n-watermark>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
</n-notification-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
|
||||
102
frontend/src/components/ClsCalendarTimeLine.vue
Normal file
102
frontend/src/components/ClsCalendarTimeLine.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import {nextTick, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
|
||||
import {ClsCalendar} from "../../wailsjs/go/main/App";
|
||||
import { addMonths, format ,parse} from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
|
||||
import {useMessage} from 'naive-ui'
|
||||
import {Star48Filled} from "@vicons/fluent";
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要+1
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
|
||||
// 常见格式:YYYY-MM-DD
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
const formattedYM = `${year}-${month}`;
|
||||
const list = ref([])
|
||||
const message=useMessage()
|
||||
|
||||
function goBackToday() {
|
||||
setTimeout(() => {
|
||||
nextTick(
|
||||
() => {
|
||||
const elementById = document.getElementById(formattedDate);
|
||||
if (elementById) {
|
||||
elementById.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'start'
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
ClsCalendar().then(res => {
|
||||
list.value = res
|
||||
goBackToday();
|
||||
})
|
||||
})
|
||||
|
||||
function getweekday(date){
|
||||
let day=parse(date, 'yyyy-MM-dd', new Date())
|
||||
return format(day, 'EEEE', {locale: zhCN})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <n-timeline size="large" style="text-align: left">-->
|
||||
<!-- <n-timeline-item v-for="item in list" :key="item.date" :title="item.date" type="info" >-->
|
||||
<!-- <n-list>-->
|
||||
<!-- <n-list-item v-for="l in item.list" :key="l.article_id ">-->
|
||||
<!-- <n-text>{{l.title}}</n-text>-->
|
||||
<!-- </n-list-item>-->
|
||||
<!-- </n-list>-->
|
||||
<!-- </n-timeline-item>-->
|
||||
<!-- </n-timeline>-->
|
||||
|
||||
<n-list bordered style="max-height: calc(100vh - 230px);text-align: left;">
|
||||
<n-scrollbar style="max-height: calc(100vh - 230px);" >
|
||||
<n-list-item v-for="(item, index) in list" :id="item.calendar_day" :key="item.calendar_day">
|
||||
<n-thing :title="item.calendar_day +' '+item.week">
|
||||
<n-list :bordered="false" hoverable>
|
||||
<n-list-item v-for="(l,i ) in item.items" :key="l.id ">
|
||||
<n-flex justify="space-between">
|
||||
<n-text :type="item.calendar_day===formattedDate?'warning':'info'">{{i+1}}# {{l.title}}
|
||||
<n-tag v-if="l.event" size="small" round type="success">事件</n-tag>
|
||||
<n-tag v-if="l.economic" size="small" round type="error">数据</n-tag>
|
||||
</n-text>
|
||||
<n-rate v-if="l.event&&(l.event.star>0)" readonly :default-value="l.event.star">
|
||||
<n-icon :component="Star48Filled"/>
|
||||
</n-rate>
|
||||
<n-rate v-if="l.economic&&(l.economic.star>0)" readonly :default-value="l.economic.star" >
|
||||
<n-icon :component="Star48Filled"/>
|
||||
</n-rate>
|
||||
</n-flex>
|
||||
|
||||
<n-flex v-if="l.economic">
|
||||
<n-tag type="warning" :bordered="false" :size="'small'">公布:{{l.economic.actual }}</n-tag>
|
||||
<n-tag type="warning" :bordered="false" :size="'small'">预测:{{l.economic.consensus}}</n-tag>
|
||||
<n-tag type="warning" :bordered="false" :size="'small'">前值:{{l.economic.front}}</n-tag>
|
||||
</n-flex>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
<n-list-item v-if="list.length==0">
|
||||
<n-text type="info">没有数据</n-text>
|
||||
</n-list-item>
|
||||
<n-list-item v-else style="text-align: center;">
|
||||
<n-button-group>
|
||||
<n-button strong secondary type="warning" @click="goBackToday">回到今天</n-button>
|
||||
</n-button-group>
|
||||
</n-list-item>
|
||||
</n-scrollbar>
|
||||
</n-list>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
133
frontend/src/components/EmbeddedUrl.vue
Normal file
133
frontend/src/components/EmbeddedUrl.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="embed-container">
|
||||
<h3 v-if="title">{{ title }}</h3>
|
||||
<div class="iframe-wrapper">
|
||||
<iframe
|
||||
:src="url"
|
||||
:title="iframeTitle"
|
||||
frameborder="0"
|
||||
scrolling="auto"
|
||||
class="embedded-iframe"
|
||||
@load="onLoad"
|
||||
@error="onError"
|
||||
:style="iframeStyle"
|
||||
></iframe>
|
||||
</div>
|
||||
<div v-if="loading" class="loading-indicator">
|
||||
<div class="spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
<p v-if="error" class="error-message">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
iframeTitle: {
|
||||
type: String,
|
||||
default: '外部内容'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
|
||||
const onLoad = () => {
|
||||
loading.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
const onError = (event) => {
|
||||
loading.value = false
|
||||
error.value = `加载失败: ${event.message || '无法加载该 URL'}`
|
||||
}
|
||||
|
||||
// 监听 URL 变化,重新加载
|
||||
watch(() => props.url, () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
})
|
||||
|
||||
// 设置 iframe 样式
|
||||
const iframeStyle = {
|
||||
width: props.width,
|
||||
height: props.height
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.embed-container {
|
||||
margin: 1rem 0;
|
||||
border: 0 solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.embedded-iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid #f3f4f6;
|
||||
border-radius: 50%;
|
||||
border-top-color: #3b82f6;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ef4444;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
background-color: #fee2e2;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
37
frontend/src/components/HotEvents.vue
Normal file
37
frontend/src/components/HotEvents.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeMount, onUnmounted, ref} from 'vue'
|
||||
import {HotEvent} from "../../wailsjs/go/main/App";
|
||||
const list = ref([])
|
||||
|
||||
const task =ref()
|
||||
onBeforeMount(async () => {
|
||||
list.value = await HotEvent(50)
|
||||
task.value=setInterval(async ()=>{
|
||||
list.value = await HotEvent(50)
|
||||
}, 1000*10)
|
||||
})
|
||||
|
||||
onUnmounted(async ()=>{
|
||||
clearInterval(task.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-list bordered>
|
||||
<template #header>
|
||||
雪球热门
|
||||
</template>
|
||||
<n-list-item v-for="(item, index) in list" :key="index">
|
||||
<n-thing :title="item.tag" :description="item.content" >
|
||||
<template v-if="item.pic" #avatar>
|
||||
<n-avatar :src="item.pic" :size="60">
|
||||
</n-avatar>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
88
frontend/src/components/HotStockList.vue
Normal file
88
frontend/src/components/HotStockList.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeMount, onUnmounted, ref} from 'vue'
|
||||
import {HotStock} from "../../wailsjs/go/main/App";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
import {ArrowBack, ArrowDown, ArrowUp} from "@vicons/ionicons5";
|
||||
|
||||
const {marketType}=defineProps(
|
||||
{
|
||||
marketType: {
|
||||
type: String,
|
||||
default: '10'
|
||||
}
|
||||
}
|
||||
)
|
||||
const task =ref()
|
||||
|
||||
const list = ref([])
|
||||
|
||||
onBeforeMount(async () => {
|
||||
list.value = await HotStock(marketType)
|
||||
task.value = setInterval(async () => {
|
||||
list.value = await HotStock(marketType)
|
||||
}, 5000)
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
clearInterval(task.value)
|
||||
})
|
||||
|
||||
function getMarketCode(item) {
|
||||
if (item.exchange === 'SZ') {
|
||||
return item.code.toLowerCase()
|
||||
}
|
||||
if (item.exchange === 'SH') {
|
||||
return item.code.toLowerCase()
|
||||
}
|
||||
if (item.exchange === 'HK') {
|
||||
return (item.exchange + item.code).toLowerCase()
|
||||
}
|
||||
return ("gb_"+item.code).toLowerCase()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-table striped size="small">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>股票名称</n-th>
|
||||
<n-th>涨跌幅</n-th>
|
||||
<n-th>当前价格</n-th>
|
||||
<n-th>热度</n-th>
|
||||
<n-th>热度变化</n-th>
|
||||
<n-th>排名变化</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in list" :key="item.code">
|
||||
<n-td><n-text type="info">
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-tag type="info" :bordered="false"> {{item.name}} {{item.code}}</n-tag>
|
||||
</template>
|
||||
<k-line-chart style="width: 800px" :code="getMarketCode(item)" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-text></n-td>
|
||||
<n-td><n-text :type="item.percent>0?'error':'success'">{{item.percent}}%</n-text></n-td>
|
||||
<n-td><n-text type="info">{{item.current}}</n-text></n-td>
|
||||
<n-td><n-text type="info">{{item.value}}</n-text></n-td>
|
||||
<n-td><n-text :type="item.increment>0?'error':'success'">
|
||||
{{item.increment}}
|
||||
<n-icon v-if="item.increment>0" :component="ArrowUp"/>
|
||||
<n-icon v-else :component="ArrowDown"/>
|
||||
</n-text></n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.rank_change>0?'error':'success'">
|
||||
{{item.rank_change}}
|
||||
<n-icon v-if="item.rank_change>0" :component="ArrowUp"/>
|
||||
<n-text v-else-if="item.rank_change==0" ></n-text>
|
||||
<n-icon v-else :component="ArrowDown"/>
|
||||
</n-text>
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
83
frontend/src/components/HotTopics.vue
Normal file
83
frontend/src/components/HotTopics.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeMount, onUnmounted, ref} from 'vue'
|
||||
import {HotTopic, OpenURL} from "../../wailsjs/go/main/App";
|
||||
import {Environment} from "../../wailsjs/runtime";
|
||||
const list = ref([])
|
||||
const task =ref()
|
||||
|
||||
onBeforeMount(async () => {
|
||||
list.value = await HotTopic(10)
|
||||
setInterval(async ()=>{
|
||||
list.value = await HotTopic(10)
|
||||
}, 1000*10)
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
clearInterval(task.value)
|
||||
})
|
||||
|
||||
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}`
|
||||
)
|
||||
break
|
||||
default:
|
||||
OpenURL(url)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
function showPage(htid) {
|
||||
openCenteredWindow(`https://gubatopic.eastmoney.com/topic_v3.html?htid=${htid}`, 1000, 600)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-list bordered hoverable clickable>
|
||||
<!-- <template #header>-->
|
||||
<!-- 股吧热门-->
|
||||
<!-- </template>-->
|
||||
<n-list-item v-for="(item, index) in list" :key="index">
|
||||
<n-thing :title="item.nickname" :description="item.desc" :description-style="'font-size: 14px;'" @click="showPage(item.htid)">
|
||||
<template v-if="item.squareImg" #avatar>
|
||||
<n-avatar :src="item.squareImg" :size="60">
|
||||
</n-avatar>
|
||||
</template>
|
||||
<template v-if="item.stock_list" #footer>
|
||||
<n-flex>
|
||||
<n-tag type="info" v-for="(v, i) in item.stock_list" :bordered="false" size="small">
|
||||
{{v.name}}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template v-if="item.clickNumber" #header-extra>
|
||||
<n-flex>
|
||||
<n-button secondary type="warning" size="tiny">讨论数:<n-number-animation
|
||||
show-separator
|
||||
:from="0"
|
||||
:to="item.postNumber"
|
||||
/>
|
||||
</n-button >
|
||||
<n-tag :bordered="false" type="warning" size="small">浏览量:<n-number-animation
|
||||
show-separator
|
||||
:from="0"
|
||||
:to="item.clickNumber"
|
||||
/>
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
115
frontend/src/components/IndustryResearchReportList.vue
Normal file
115
frontend/src/components/IndustryResearchReportList.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script setup>
|
||||
import {onBeforeMount, ref} from 'vue'
|
||||
import {GetStockList, IndustryResearchReport,EMDictCode} from "../../wailsjs/go/main/App";
|
||||
import {ArrowDownOutline, CaretDown, CaretUp, PulseOutline, Refresh, RefreshCircleSharp,} from "@vicons/ionicons5";
|
||||
|
||||
import {useMessage} from "naive-ui";
|
||||
import {BrowserOpenURL} from "../../wailsjs/runtime";
|
||||
|
||||
const message=useMessage()
|
||||
const list = ref([])
|
||||
|
||||
const options = ref([])
|
||||
|
||||
function getIndustryResearchReport(value) {
|
||||
message.loading("正在刷新数据...")
|
||||
IndustryResearchReport(value).then(result => {
|
||||
console.log(result)
|
||||
list.value = result
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeMount(()=>{
|
||||
getIndustryResearchReport('');
|
||||
})
|
||||
|
||||
function ratingChangeName(ratingChange){
|
||||
if(ratingChange===0){
|
||||
return '调高'
|
||||
}else if(ratingChange===1){
|
||||
return '调低'
|
||||
}else if(ratingChange===2){
|
||||
return '首次'
|
||||
}else if(ratingChange===3){
|
||||
return '维持'
|
||||
}else if (ratingChange===4){
|
||||
return '无变化'
|
||||
}else{
|
||||
return ''
|
||||
}
|
||||
}
|
||||
function openWin(code) {
|
||||
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H3_"+code+"_1.pdf?1749744888000.pdf")
|
||||
}
|
||||
|
||||
function EMDictCodeList(keyVal){
|
||||
if (keyVal){
|
||||
EMDictCode('016').then(result => {
|
||||
console.log(result)
|
||||
options.value=result.filter((value,index,array) => value.bkName.includes(keyVal)||value.firstLetter.includes(keyVal)||value.bkCode.includes(keyVal)).map(item => {
|
||||
return {
|
||||
label: item.bkName+" - "+item.bkCode,
|
||||
value: item.bkCode
|
||||
}
|
||||
})
|
||||
})
|
||||
}else{
|
||||
getIndustryResearchReport('')
|
||||
}
|
||||
|
||||
}
|
||||
function handleSearch(value) {
|
||||
getIndustryResearchReport(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<n-auto-complete :options="options" placeholder="请输入行业名称关键词搜索" clearable filterable :on-select="handleSearch" :on-update:value="EMDictCodeList" />
|
||||
</n-card>
|
||||
<n-table striped size="small">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<!-- <n-th>代码</n-th>-->
|
||||
<!-- <n-th>名称</n-th>-->
|
||||
<n-th>行业</n-th>
|
||||
<n-th>标题</n-th>
|
||||
<n-th>东财评级</n-th>
|
||||
<n-th>评级变动</n-th>
|
||||
<n-th>机构评级</n-th>
|
||||
<n-th>分析师</n-th>
|
||||
<n-th>机构</n-th>
|
||||
<n-th> <n-flex justify="space-between">日期<n-icon @click="getIndustryResearchReport" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in list" :key="item.infoCode">
|
||||
<!-- <n-td>{{item.stockCode}}</n-td>-->
|
||||
<!-- <n-td :title="item.stockCode">-->
|
||||
<!-- <n-popover trigger="hover" placement="right">-->
|
||||
<!-- <template #trigger>-->
|
||||
<!-- <n-tag type="info" :bordered="false">{{item.stockName}}</n-tag>-->
|
||||
<!-- </template>-->
|
||||
<!-- <k-line-chart style="width: 800px" :code="getmMarketCode(item.market,item.stockCode)" :chart-height="500" :name="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>-->
|
||||
<!-- </n-popover>-->
|
||||
<!-- </n-td>-->
|
||||
<n-td><n-tag type="info" :bordered="false">{{item.industryName}}</n-tag></n-td>
|
||||
<n-td>
|
||||
<n-a type="info" @click="openWin(item.infoCode)"><n-text type="success">{{item.title}}</n-text></n-a>
|
||||
</n-td>
|
||||
<n-td><n-text :type="item.emRatingName==='增持'?'error':'info'">
|
||||
{{item.emRatingName}}
|
||||
</n-text></n-td>
|
||||
<n-td><n-text :type="item.ratingChange===0?'error':'info'">{{ratingChangeName(item.ratingChange)}}</n-text></n-td>
|
||||
<n-td>{{item.sRatingName }}</n-td>
|
||||
<n-td><n-ellipsis style="max-width: 120px">{{item.researcher}}</n-ellipsis></n-td>
|
||||
<n-td>{{item.orgSName}}</n-td>
|
||||
<n-td>{{item.publishDate.substring(0,10)}}</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
108
frontend/src/components/InvestCalendarTimeLine.vue
Normal file
108
frontend/src/components/InvestCalendarTimeLine.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import {nextTick, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
|
||||
import {InvestCalendarTimeLine} from "../../wailsjs/go/main/App";
|
||||
import { addMonths, format ,parse} from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
|
||||
import {useMessage} from 'naive-ui'
|
||||
import {Star48Filled} from "@vicons/fluent";
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要+1
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
|
||||
// 常见格式:YYYY-MM-DD
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
const formattedYM = `${year}-${month}`;
|
||||
const list = ref([])
|
||||
const message=useMessage()
|
||||
|
||||
function goBackToday() {
|
||||
setTimeout(() => {
|
||||
nextTick(
|
||||
() => {
|
||||
const elementById = document.getElementById(formattedDate);
|
||||
if (elementById) {
|
||||
elementById.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'start'
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
InvestCalendarTimeLine(formattedYM).then(res => {
|
||||
list.value = res
|
||||
goBackToday();
|
||||
})
|
||||
})
|
||||
onMounted(()=>{
|
||||
|
||||
})
|
||||
function loadMore(){
|
||||
if (list.value.length>0){
|
||||
let day=parse(list.value[list.value.length-1].date, 'yyyy-MM-dd', new Date())
|
||||
let nextMonth=addMonths(day,1)
|
||||
let ym = format(nextMonth, 'yyyy-MM');
|
||||
console.log(ym)
|
||||
InvestCalendarTimeLine(ym).then(res => {
|
||||
if (res.length==0){
|
||||
message.warning("没有更多数据了")
|
||||
return
|
||||
}
|
||||
list.value.push( ...res)
|
||||
})
|
||||
}
|
||||
}
|
||||
function getweekday(date){
|
||||
let day=parse(date, 'yyyy-MM-dd', new Date())
|
||||
return format(day, 'EEEE', {locale: zhCN})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <n-timeline size="large" style="text-align: left">-->
|
||||
<!-- <n-timeline-item v-for="item in list" :key="item.date" :title="item.date" type="info" >-->
|
||||
<!-- <n-list>-->
|
||||
<!-- <n-list-item v-for="l in item.list" :key="l.article_id ">-->
|
||||
<!-- <n-text>{{l.title}}</n-text>-->
|
||||
<!-- </n-list-item>-->
|
||||
<!-- </n-list>-->
|
||||
<!-- </n-timeline-item>-->
|
||||
<!-- </n-timeline>-->
|
||||
|
||||
<n-list bordered style="max-height: calc(100vh - 230px);text-align: left;">
|
||||
<n-scrollbar style="max-height: calc(100vh - 230px);" >
|
||||
<n-list-item v-for="(item, index) in list" :id="item.date" :key="item.date">
|
||||
<n-thing :title="item.date+' '+getweekday(item.date)">
|
||||
<n-list :bordered="false" hoverable>
|
||||
<n-list-item v-for="(l,i ) in item.list" :key="l.article_id ">
|
||||
<n-flex justify="space-between">
|
||||
<n-text :type="item.date===formattedDate?'warning':'info'">{{i+1}}# {{l.title}}</n-text>
|
||||
<n-rate v-if="l.like_count>0" readonly :default-value="l.like_count" :count="l.like_count" >
|
||||
<n-icon :component="Star48Filled"/>
|
||||
</n-rate>
|
||||
</n-flex>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
<n-list-item v-if="list.length==0">
|
||||
<n-text type="info">没有数据</n-text>
|
||||
</n-list-item>
|
||||
<n-list-item v-else style="text-align: center;">
|
||||
<n-button-group>
|
||||
<n-button strong secondary type="info" @click="loadMore">加载更多</n-button>
|
||||
<n-button strong secondary type="warning" @click="goBackToday">回到今天</n-button>
|
||||
</n-button-group>
|
||||
</n-list-item>
|
||||
</n-scrollbar>
|
||||
</n-list>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
386
frontend/src/components/KLineChart.vue
Normal file
386
frontend/src/components/KLineChart.vue
Normal file
@@ -0,0 +1,386 @@
|
||||
<script setup>
|
||||
|
||||
import {GetStockKLine} from "../../wailsjs/go/main/App";
|
||||
import * as echarts from "echarts";
|
||||
import {onMounted, ref} from "vue";
|
||||
import _ from "lodash";
|
||||
const { code,name,darkTheme,kDays ,chartHeight} = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
kDays: {
|
||||
type: Number,
|
||||
default: 14
|
||||
},
|
||||
chartHeight: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
darkTheme: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const upColor = '#ec0000';
|
||||
const upBorderColor = '';
|
||||
const downColor = '#00da3c';
|
||||
const downBorderColor = '';
|
||||
const kLineChartRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
handleKLine(code,name)
|
||||
})
|
||||
|
||||
function handleKLine(code,name){
|
||||
GetStockKLine(code,name,365).then(result => {
|
||||
//console.log("GetStockKLine",result)
|
||||
const chart = echarts.init(kLineChartRef.value);
|
||||
const categoryData = [];
|
||||
const values = [];
|
||||
const volumns=[];
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
let resultElement=result[i]
|
||||
//console.log("resultElement:{}",resultElement)
|
||||
categoryData.push(resultElement.day)
|
||||
let flag=resultElement.close>resultElement.open?1:-1
|
||||
values.push([
|
||||
resultElement.open,
|
||||
resultElement.close,
|
||||
resultElement.low,
|
||||
resultElement.high
|
||||
])
|
||||
volumns.push([i,resultElement.volume/10000,flag])
|
||||
}
|
||||
////console.log("categoryData",categoryData)
|
||||
////console.log("values",values)
|
||||
let option = {
|
||||
title: {
|
||||
text: name+" "+code,
|
||||
left: '20px',
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
}
|
||||
},
|
||||
darkMode: darkTheme,
|
||||
//backgroundColor: '#1c1c1c',
|
||||
// color:['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
|
||||
animation: false,
|
||||
legend: {
|
||||
right: 20,
|
||||
top: 0,
|
||||
data: ['日K', 'MA5', 'MA10', 'MA20', 'MA30'],
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
lineStyle: {
|
||||
color: '#376df4',
|
||||
width: 2,
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
borderWidth: 2,
|
||||
borderColor: darkTheme?'#456':'#ccc',
|
||||
backgroundColor: darkTheme?'#456':'#fff',
|
||||
padding: 10,
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
},
|
||||
formatter: function (params) {//修改鼠标划过显示为中文
|
||||
//console.log("params",params)
|
||||
let currentItemData = _.filter(params, (param) => param.seriesIndex === 0)[0].data;
|
||||
let ma5=_.filter(params, (param) => param.seriesIndex === 1)[0].data;//ma5的值
|
||||
let ma10=_.filter(params, (param) => param.seriesIndex === 2)[0].data;//ma10的值
|
||||
let ma20=_.filter(params, (param) => param.seriesIndex === 3)[0].data;//ma20的值
|
||||
let ma30=_.filter(params, (param) => param.seriesIndex === 4)[0].data;//ma30的值
|
||||
let volum=_.filter(params, (param) => param.seriesIndex === 5)[0].data;
|
||||
return _.filter(params, (param) => param.seriesIndex === 0)[0].name + '<br>' +
|
||||
'开盘:' + currentItemData[1] + '<br>' +
|
||||
'收盘:' + currentItemData[2] + '<br>' +
|
||||
'最低:' + currentItemData[3] + '<br>' +
|
||||
'最高:' + currentItemData[4] + '<br>' +
|
||||
'成交量(万手):' + volum[1] + '<br>' +
|
||||
'MA5日均线:' + ma5 + '<br>' +
|
||||
'MA10日均线:' + ma10 + '<br>' +
|
||||
'MA20日均线:' + ma20 + '<br>' +
|
||||
'MA30日均线:' + ma30
|
||||
}
|
||||
},
|
||||
axisPointer: {
|
||||
link: [
|
||||
{
|
||||
xAxisIndex: 'all'
|
||||
}
|
||||
],
|
||||
label: {
|
||||
backgroundColor: '#888'
|
||||
}
|
||||
},
|
||||
visualMap: {
|
||||
show: false,
|
||||
seriesIndex: 5,
|
||||
dimension: 2,
|
||||
pieces: [
|
||||
{
|
||||
value: -1,
|
||||
color: downColor
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
color: upColor
|
||||
}
|
||||
]
|
||||
},
|
||||
grid: [
|
||||
{
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
height: '50%',
|
||||
},
|
||||
{
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
top: '66%',
|
||||
height: '15%'
|
||||
}
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: categoryData,
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: false },
|
||||
splitLine: { show: false },
|
||||
min: 'dataMin',
|
||||
max: 'dataMax',
|
||||
axisPointer: {
|
||||
z: 100
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
gridIndex: 1,
|
||||
data: categoryData,
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
min: 'dataMin',
|
||||
max: 'dataMax'
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
splitArea: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
{
|
||||
scale: true,
|
||||
gridIndex: 1,
|
||||
splitNumber: 2,
|
||||
axisLabel: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false }
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0, 1],
|
||||
start: 100-kDays,
|
||||
end: 100
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
xAxisIndex: [0, 1],
|
||||
type: 'slider',
|
||||
top: '85%',
|
||||
start: 100-kDays,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
|
||||
series: [
|
||||
{
|
||||
name: '日K',
|
||||
type: 'candlestick',
|
||||
data: values,
|
||||
itemStyle: {
|
||||
color: upColor,
|
||||
color0: downColor,
|
||||
// borderColor: upBorderColor,
|
||||
// borderColor0: downBorderColor
|
||||
},
|
||||
markPoint: {
|
||||
//symbol: 'none',
|
||||
label: {
|
||||
formatter: function (param) {
|
||||
return param != null ? param.value + '' : '';
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{
|
||||
name: '最高',
|
||||
type: 'max',
|
||||
valueDim: 'highest'
|
||||
},
|
||||
{
|
||||
name: '最低',
|
||||
type: 'min',
|
||||
valueDim: 'lowest'
|
||||
},
|
||||
{
|
||||
name: '平均收盘价',
|
||||
type: 'average',
|
||||
valueDim: 'close'
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
formatter: function (param) {
|
||||
return param.name + '<br>' + (param.data.coord || '');
|
||||
}
|
||||
}
|
||||
},
|
||||
markLine: {
|
||||
symbol: ['none', 'none'],
|
||||
data: [
|
||||
[
|
||||
{
|
||||
name: 'from lowest to highest',
|
||||
type: 'min',
|
||||
valueDim: 'lowest',
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'max',
|
||||
valueDim: 'highest',
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
name: 'min line on close',
|
||||
type: 'min',
|
||||
valueDim: 'close'
|
||||
},
|
||||
{
|
||||
name: 'max line on close',
|
||||
type: 'max',
|
||||
valueDim: 'close'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MA5',
|
||||
type: 'line',
|
||||
data: calculateMA(5,values),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
opacity: 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MA10',
|
||||
type: 'line',
|
||||
data: calculateMA(10,values),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
opacity: 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MA20',
|
||||
type: 'line',
|
||||
data: calculateMA(20,values),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
opacity: 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MA30',
|
||||
type: 'line',
|
||||
data: calculateMA(30,values),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
opacity: 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '成交量(手)',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
itemStyle: {
|
||||
color: '#7fbe9e'
|
||||
},
|
||||
data: volumns
|
||||
}
|
||||
]
|
||||
};
|
||||
chart.setOption(option);
|
||||
|
||||
chart.on('click',{seriesName:'日K'}, function(params) {
|
||||
//console.log("click:",params);
|
||||
});
|
||||
})
|
||||
}
|
||||
function calculateMA(dayCount,values) {
|
||||
var result = [];
|
||||
for (var i = 0, len = values.length; i < len; i++) {
|
||||
if (i < dayCount) {
|
||||
result.push('-');
|
||||
continue;
|
||||
}
|
||||
var sum = 0;
|
||||
for (var j = 0; j < dayCount; j++) {
|
||||
sum += +values[i - j][1];
|
||||
}
|
||||
result.push((sum / dayCount).toFixed(2));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="kLineChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
229
frontend/src/components/LongTigerRankList.vue
Normal file
229
frontend/src/components/LongTigerRankList.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeMount, ref} from 'vue'
|
||||
import {LongTigerRank} from "../../wailsjs/go/main/App";
|
||||
import {BrowserOpenURL} from "../../wailsjs/runtime";
|
||||
import {ArrowDownOutline} from "@vicons/ionicons5";
|
||||
import _ from "lodash";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
import MoneyTrend from "./moneyTrend.vue";
|
||||
import {NButton, NText, useMessage} from "naive-ui";
|
||||
const message = useMessage()
|
||||
|
||||
const lhbList= ref([])
|
||||
const EXPLANATIONs = ref([])
|
||||
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要+1
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
|
||||
// 常见格式:YYYY-MM-DD
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
|
||||
const SearchForm= ref({
|
||||
dateValue: formattedDate,
|
||||
EXPLANATION:null,
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
longTiger(formattedDate);
|
||||
})
|
||||
function longTiger_old(date) {
|
||||
if(date) {
|
||||
SearchForm.value.dateValue = date
|
||||
}
|
||||
let loading1=message.loading("正在获取龙虎榜数据...",{
|
||||
duration: 0,
|
||||
})
|
||||
LongTigerRank(date).then(res => {
|
||||
lhbList.value = res
|
||||
loading1.destroy()
|
||||
if (res.length === 0) {
|
||||
message.info("暂无数据,请切换日期")
|
||||
}
|
||||
EXPLANATIONs.value=_.uniqBy(_.map(lhbList.value,function (item){
|
||||
return {
|
||||
label: item['EXPLANATION'],
|
||||
value: item['EXPLANATION'],
|
||||
}
|
||||
}),'label');
|
||||
})
|
||||
}
|
||||
|
||||
function longTiger(date) {
|
||||
if (date) {
|
||||
SearchForm.value.dateValue = date;
|
||||
}
|
||||
|
||||
let loading1 = message.loading("正在获取龙虎榜数据...", {
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
const fetchDate = (currentDate, retryCount = 0) => {
|
||||
if (retryCount > 7) { // 防止无限循环,最多尝试7次
|
||||
lhbList.value = [];
|
||||
EXPLANATIONs.value = [];
|
||||
loading1.destroy();
|
||||
message.info("暂无历史数据");
|
||||
return;
|
||||
}
|
||||
|
||||
LongTigerRank(currentDate).then(res => {
|
||||
if (res.length === 0) {
|
||||
const previousDate = new Date(currentDate);
|
||||
previousDate.setDate(previousDate.getDate() - 1);
|
||||
|
||||
const year = previousDate.getFullYear();
|
||||
const month = String(previousDate.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(previousDate.getDate()).padStart(2, '0');
|
||||
const prevFormattedDate = `${year}-${month}-${day}`;
|
||||
|
||||
message.info(`当前日期 ${currentDate} 暂无数据,尝试查询前一日:${prevFormattedDate}`);
|
||||
|
||||
SearchForm.value.dateValue = prevFormattedDate;
|
||||
fetchDate(prevFormattedDate, retryCount + 1); // 递归调用
|
||||
} else {
|
||||
lhbList.value = res;
|
||||
loading1.destroy();
|
||||
EXPLANATIONs.value = _.uniqBy(_.map(lhbList.value, function (item) {
|
||||
return {
|
||||
label: item['EXPLANATION'],
|
||||
value: item['EXPLANATION'],
|
||||
};
|
||||
}), 'label');
|
||||
}
|
||||
}).catch(err => {
|
||||
loading1.destroy();
|
||||
message.error("获取数据失败,请重试");
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
fetchDate(date || formattedDate);
|
||||
}
|
||||
|
||||
function handleEXPLANATION(value, option){
|
||||
SearchForm.value.EXPLANATION = value
|
||||
if(value){
|
||||
LongTigerRank(SearchForm.value.dateValue).then(res => {
|
||||
lhbList.value=_.filter(res, function(o) { return o['EXPLANATION']===value; });
|
||||
if (res.length === 0) {
|
||||
message.info("暂无数据,请切换日期")
|
||||
}
|
||||
})
|
||||
}else{
|
||||
longTiger(SearchForm.value.dateValue)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-form :model="SearchForm" >
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<n-form-item-gi :span="4" label="日期" path="dateValue" label-placement="left">
|
||||
<n-date-picker v-model:formatted-value="SearchForm.dateValue"
|
||||
value-format="yyyy-MM-dd" type="date" :on-update:value="(v,v2)=>longTiger(v2)"/>
|
||||
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="8" label="上榜原因" path="EXPLANATION" label-placement="left">
|
||||
<n-select clearable placeholder="上榜原因过滤" v-model:value="SearchForm.EXPLANATION" :options="EXPLANATIONs" :on-update:value="handleEXPLANATION"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" label="" label-placement="left">
|
||||
<n-text type="error">*当天的龙虎榜数据通常在收盘结束后一小时左右更新</n-text>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
<n-table :single-line="false" striped>
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>代码</n-th>
|
||||
<!-- <n-th width="90px">日期</n-th>-->
|
||||
<n-th width="60px">名称</n-th>
|
||||
<n-th>收盘价</n-th>
|
||||
<n-th width="60px">涨跌幅</n-th>
|
||||
<n-th>龙虎榜净买额(万)</n-th>
|
||||
<n-th>龙虎榜买入额(万)</n-th>
|
||||
<n-th>龙虎榜卖出额(万)</n-th>
|
||||
<n-th>龙虎榜成交额(万)</n-th>
|
||||
<!-- <n-th>市场总成交额(万)</n-th>-->
|
||||
<!-- <n-th>净买额占总成交比</n-th>-->
|
||||
<!-- <n-th>成交额占总成交比</n-th>-->
|
||||
<n-th width="60px" data-field="TURNOVERRATE">换手率<n-icon :component="ArrowDownOutline" /></n-th>
|
||||
<n-th>流通市值(亿)</n-th>
|
||||
<n-th>上榜原因</n-th>
|
||||
<!-- <n-th>解读</n-th>-->
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="(item, index) in lhbList" :key="index">
|
||||
<n-td>
|
||||
<n-tag :bordered=false type="info">{{ item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0] }}</n-tag>
|
||||
</n-td>
|
||||
<!-- <n-td>
|
||||
{{item.TRADE_DATE.substring(0,10)}}
|
||||
</n-td>-->
|
||||
<n-td>
|
||||
<!-- <n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.SECURITY_NAME_ABBR }}</n-text>-->
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-button tag="a" text :type="item.CHANGE_RATE>0?'error':'success'" :bordered=false >{{ item.SECURITY_NAME_ABBR }}</n-button>
|
||||
</template>
|
||||
<k-line-chart style="width: 800px" :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :chart-height="500" :name="item.SECURITY_NAME_ABBR" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.CLOSE_PRICE }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ (item.CHANGE_RATE).toFixed(2) }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<!-- <n-text :type="item.BILLBOARD_NET_AMT>0?'error':'success'">{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-text>-->
|
||||
|
||||
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-button tag="a" text :type="item.BILLBOARD_NET_AMT>0?'error':'success'" :bordered=false >{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-button>
|
||||
</template>
|
||||
<money-trend :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :name="item.SECURITY_NAME_ABBR" :days="360" :dark-theme="true" :chart-height="500" style="width: 800px"></money-trend>
|
||||
</n-popover>
|
||||
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'error'">{{ (item.BILLBOARD_BUY_AMT/10000).toFixed(2) }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'success'">{{ (item.BILLBOARD_SELL_AMT/10000).toFixed(2) }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'info'">{{ (item.BILLBOARD_DEAL_AMT/10000).toFixed(2) }}</n-text>
|
||||
</n-td>
|
||||
<!-- <n-td>-->
|
||||
<!-- <n-text :type="'info'">{{ (item.ACCUM_AMOUNT/10000).toFixed(2) }}</n-text>-->
|
||||
<!-- </n-td>-->
|
||||
<!-- <n-td>-->
|
||||
<!-- <n-text :type="item.DEAL_NET_RATIO>0?'error':'success'">{{ (item.DEAL_NET_RATIO).toFixed(2) }}%</n-text>-->
|
||||
<!-- </n-td>-->
|
||||
<!-- <n-td>-->
|
||||
<!-- <n-text :type="'info'">{{ (item.DEAL_AMOUNT_RATIO).toFixed(2) }}%</n-text>-->
|
||||
<!-- </n-td>-->
|
||||
<n-td>
|
||||
<n-text :type="'info'">{{ (item.TURNOVERRATE).toFixed(2) }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'info'">{{ (item.FREE_MARKET_CAP/100000000).toFixed(2) }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'info'">{{ item.EXPLANATION }}</n-text>
|
||||
</n-td>
|
||||
<!-- <n-td>
|
||||
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.EXPLAIN }}</n-text>
|
||||
</n-td>-->
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
233
frontend/src/components/SelectStock.vue
Normal file
233
frontend/src/components/SelectStock.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<script setup lang="ts">
|
||||
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
|
||||
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) {
|
||||
message.warning('请输入选股指标或者要求')
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
return {
|
||||
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
|
||||
key: item.key,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
children: item.children.filter(item => !item.hiddenNeed).map(item => {
|
||||
return {
|
||||
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])) {
|
||||
return row1[item.key] - row2[item.key];
|
||||
} else {
|
||||
return 'default'
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
|
||||
key: item.key,
|
||||
resizable: true,
|
||||
minWidth: 120,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
sorter: (row1, row2) => {
|
||||
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
|
||||
return row1[item.key] - row2[item.key];
|
||||
} else {
|
||||
return 'default'
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
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);
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
GetHotStrategy().then(res => {
|
||||
console.log(res)
|
||||
if (res.code == 1) {
|
||||
hotStrategy.value = res.data
|
||||
search.value = hotStrategy.value[0].question
|
||||
Search()
|
||||
}
|
||||
}).catch(err => {
|
||||
message.error(err)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
function DoSearch(question) {
|
||||
search.value = question
|
||||
Search()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>-->
|
||||
|
||||
<!-- </template>-->
|
||||
<!-- </n-virtual-list>-->
|
||||
</n-gi>
|
||||
<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-button type="primary" @click="Search">搜索A股</n-button>
|
||||
</n-input-group>
|
||||
</n-flex>
|
||||
<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>
|
||||
<template #tooltip>
|
||||
<div style="text-align: center;max-width: 580px">
|
||||
<n-text type="warning">{{ traceInfo }}</n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
|
||||
<!-- <n-button type="primary" size="small">保存策略</n-button>-->
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
:striped="true"
|
||||
:max-height="'calc(100vh - 150px)'"
|
||||
size="medium"
|
||||
:columns="columns"
|
||||
:data="dataList"
|
||||
:pagination="{pageSize: 10}"
|
||||
:scroll-x="1800"
|
||||
:render-cell="(value, rowData, column) => {
|
||||
|
||||
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
|
||||
return h(NText, { type: 'info',border: false }, { default: () => `${value}` })
|
||||
}
|
||||
if (isNumeric(value)) {
|
||||
let type='info';
|
||||
if (Number(value)<0){
|
||||
type='success';
|
||||
}
|
||||
if(Number(value)>=0&&Number(value)<=5){
|
||||
type='warning';
|
||||
}
|
||||
if (Number(value)>5){
|
||||
type='error';
|
||||
}
|
||||
return h(NText, { type: type }, { default: () => `${value}` })
|
||||
}else{
|
||||
if(column.key=='SECURITY_SHORT_NAME'){
|
||||
return h(NButton, { type: 'info',bordered: false ,size:'small',onClick:()=>{
|
||||
//https://quote.eastmoney.com/sz300558.html#fullScreenChart
|
||||
openCenteredWindow(`https://quote.eastmoney.com/${rowData.MARKET_SHORT_NAME}${rowData.SECURITY_CODE}.html#fullScreenChart`,1240,700)
|
||||
}}, { default: () => `${value}` })
|
||||
}else{
|
||||
return h(NText, { type: 'info' }, { default: () => `${value}` })
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<div style="margin-top: -25px">共找到
|
||||
<n-tag type="info" :bordered="false">{{ dataList.length }}</n-tag>
|
||||
只股
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
148
frontend/src/components/StockNoticeList.vue
Normal file
148
frontend/src/components/StockNoticeList.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeMount, ref} from 'vue'
|
||||
import {GetStockList, StockNotice} from "../../wailsjs/go/main/App";
|
||||
import {BrowserOpenURL} from "../../wailsjs/runtime";
|
||||
import {RefreshCircleSharp} from "@vicons/ionicons5";
|
||||
import _ from "lodash";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
import MoneyTrend from "./moneyTrend.vue";
|
||||
import {useMessage} from "naive-ui";
|
||||
|
||||
const {stockCode}=defineProps(
|
||||
{
|
||||
stockCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const list = ref([])
|
||||
const options = ref([])
|
||||
const message=useMessage()
|
||||
function getNotice(stockCodes) {
|
||||
StockNotice(stockCodes).then(result => {
|
||||
console.log(result)
|
||||
list.value = result
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeMount (()=>{
|
||||
//message.info("正在获取数据"+stockCode)
|
||||
getNotice(stockCode);
|
||||
})
|
||||
|
||||
function findStockList(query){
|
||||
if (query){
|
||||
GetStockList(query).then(result => {
|
||||
options.value=result.map(item => {
|
||||
return {
|
||||
label: item.name+" - "+item.ts_code,
|
||||
value: item.ts_code
|
||||
}
|
||||
})
|
||||
})
|
||||
}else{
|
||||
getNotice("")
|
||||
}
|
||||
}
|
||||
function handleSearch(value) {
|
||||
getNotice(value)
|
||||
}
|
||||
function openWin(code) {
|
||||
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H2_"+code+"_1.pdf?1750092081000.pdf")
|
||||
}
|
||||
function getTypeColor(name){
|
||||
if(name.includes("质押")||name.includes("冻结")||name.includes("解冻")||name.includes("解押")||name.includes("解禁")){
|
||||
return "error"
|
||||
}
|
||||
if(name.includes("异常")||name.includes("减持")||name.includes("增发")||name.includes("重大")){
|
||||
return "error"
|
||||
}
|
||||
if(name.includes("季度报告")||name.includes("年度报告")||name.includes("澄清公告")||name.includes("风险")){
|
||||
return "error"
|
||||
}
|
||||
if(name.includes("终止")||name.includes("复牌")||name.includes("停牌")||name.includes("退市")){
|
||||
return "error"
|
||||
}
|
||||
if(name.includes("破产")||name.includes("清算")){
|
||||
return "error"
|
||||
}
|
||||
if(name.includes("回购")||name.includes("重组")||name.includes("诉讼")||name.includes("仲裁")||name.includes("转让")||name.includes("收购")){
|
||||
return "warning"
|
||||
}
|
||||
if(name.includes("调研")||name.includes("募集")){
|
||||
return "warning"
|
||||
}
|
||||
|
||||
return "info"
|
||||
|
||||
}
|
||||
function getmMarketCode(market,code) {
|
||||
if(market==="0"){
|
||||
return "sz"+code
|
||||
}else if(market==="1"){
|
||||
return "sh"+code
|
||||
}else if(market==="2"){
|
||||
return "bj"+code
|
||||
}else if(market==="3"){
|
||||
return "hk"+code
|
||||
}else{
|
||||
return code
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<n-auto-complete :options="options" placeholder="请输入A股名称或者代码" clearable filterable :on-select="handleSearch" :on-update:value="findStockList" />
|
||||
</n-card>
|
||||
<n-table striped size="small">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>股票代码</n-th>
|
||||
<n-th>股票名称</n-th>
|
||||
<n-th>公告标题</n-th>
|
||||
<n-th>公告类型</n-th>
|
||||
<n-th>公告日期</n-th>
|
||||
<n-th><n-flex>数据更新时间<n-icon @click="getNotice('')" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in list" :key="item.art_code">
|
||||
<n-td>
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-tag type="info" :bordered="false">{{item.codes[0].stock_code }}</n-tag>
|
||||
</template>
|
||||
<money-trend style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :name="item.codes[0].short_name" :days="360" :dark-theme="true" :chart-height="500"></money-trend>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-tag type="info" :bordered="false">{{item.codes[0].short_name }}</n-tag>
|
||||
</template>
|
||||
<k-line-chart style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :chart-height="500" :name="item.codes[0].short_name" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-a type="info" @click="openWin(item.art_code)"><n-text :type="getTypeColor(item.columns[0].column_name)"> {{item.title}}</n-text></n-a>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="getTypeColor(item.columns[0].column_name)">{{item.columns[0].column_name }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-tag type="info">{{item.notice_date.substring(0,10) }}</n-tag>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-tag type="info">{{item.display_time.substring(0,19)}}</n-tag>
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
136
frontend/src/components/StockResearchReportList.vue
Normal file
136
frontend/src/components/StockResearchReportList.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup>
|
||||
import {onBeforeMount, ref} from 'vue'
|
||||
import {GetStockList, StockResearchReport} from "../../wailsjs/go/main/App";
|
||||
import {ArrowDownOutline, CaretDown, CaretUp, PulseOutline, Refresh, RefreshCircleSharp,} from "@vicons/ionicons5";
|
||||
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
import MoneyTrend from "./moneyTrend.vue";
|
||||
import {useMessage} from "naive-ui";
|
||||
import {BrowserOpenURL} from "../../wailsjs/runtime";
|
||||
|
||||
const {stockCode}=defineProps(
|
||||
{
|
||||
stockCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const message=useMessage()
|
||||
const list = ref([])
|
||||
|
||||
const options = ref([])
|
||||
|
||||
function getStockResearchReport(value) {
|
||||
StockResearchReport(value).then(result => {
|
||||
//console.log(result)
|
||||
list.value = result
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeMount(()=>{
|
||||
getStockResearchReport(stockCode);
|
||||
})
|
||||
|
||||
function ratingChangeName(ratingChange){
|
||||
if(ratingChange===0){
|
||||
return '调高'
|
||||
}else if(ratingChange===1){
|
||||
return '调低'
|
||||
}else if(ratingChange===2){
|
||||
return '首次'
|
||||
}else if(ratingChange===3){
|
||||
return '维持'
|
||||
}else if (ratingChange===4){
|
||||
return '无变化'
|
||||
}else{
|
||||
return ''
|
||||
}
|
||||
}
|
||||
function getmMarketCode(market,code) {
|
||||
if(market==="SHENZHEN"){
|
||||
return "sz"+code
|
||||
}else if(market==="SHANGHAI"){
|
||||
return "sh"+code
|
||||
}else if(market==="BEIJING"){
|
||||
return "bj"+code
|
||||
}else if(market==="HONGKONG"){
|
||||
return "hk"+code
|
||||
}else{
|
||||
return code
|
||||
}
|
||||
}
|
||||
function openWin(code) {
|
||||
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H3_"+code+"_1.pdf?1749744888000.pdf")
|
||||
}
|
||||
|
||||
function findStockList(query){
|
||||
if (query){
|
||||
GetStockList(query).then(result => {
|
||||
options.value=result.map(item => {
|
||||
return {
|
||||
label: item.name+" - "+item.ts_code,
|
||||
value: item.ts_code
|
||||
}
|
||||
})
|
||||
})
|
||||
}else{
|
||||
getStockResearchReport('')
|
||||
}
|
||||
}
|
||||
function handleSearch(value) {
|
||||
getStockResearchReport(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<n-auto-complete :options="options" placeholder="请输入A股名称或者代码" clearable filterable :on-select="handleSearch" :on-update:value="findStockList" />
|
||||
</n-card>
|
||||
<n-table striped size="small">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<!-- <n-th>代码</n-th>-->
|
||||
<n-th>名称</n-th>
|
||||
<n-th>行业</n-th>
|
||||
<n-th>标题</n-th>
|
||||
<n-th>东财评级</n-th>
|
||||
<n-th>评级变动</n-th>
|
||||
<n-th>机构评级</n-th>
|
||||
<n-th>分析师</n-th>
|
||||
<n-th>机构</n-th>
|
||||
<n-th> <n-flex justify="space-between">日期<n-icon @click="getStockResearchReport" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in list" :key="item.infoCode">
|
||||
<!-- <n-td>{{item.stockCode}}</n-td>-->
|
||||
<n-td :title="item.stockCode">
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-tag type="info" :bordered="false">{{item.stockName}}</n-tag>
|
||||
</template>
|
||||
<k-line-chart style="width: 800px" :code="getmMarketCode(item.market,item.stockCode)" :chart-height="500" :name="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td><n-tag type="info" :bordered="false">{{item.indvInduName}}</n-tag></n-td>
|
||||
<n-td>
|
||||
<n-a type="info" @click="openWin(item.infoCode)">{{item.title}}</n-a>
|
||||
</n-td>
|
||||
<n-td><n-text :type="item.emRatingName==='增持'?'error':'info'">
|
||||
{{item.emRatingName}}
|
||||
</n-text></n-td>
|
||||
<n-td><n-text :type="item.ratingChange===0?'error':'info'">{{ratingChangeName(item.ratingChange)}}</n-text></n-td>
|
||||
<n-td>{{item.sRatingName}}</n-td>
|
||||
<n-td>{{item.researcher}}</n-td>
|
||||
<n-td>{{item.orgSName}}</n-td>
|
||||
<n-td>{{item.publishDate.substring(0,10)}}</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -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 {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,10 +25,22 @@ 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()
|
||||
EventsOff("updateVersion")
|
||||
})
|
||||
|
||||
EventsOn("updateVersion",async (msg) => {
|
||||
@@ -43,8 +59,8 @@ EventsOn("updateVersion",async (msg) => {
|
||||
|
||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
|
||||
console.log("GitHub UTC 时间:", utcDate);
|
||||
console.log("转换后的本地时间:", formattedDate);
|
||||
//console.log("GitHub UTC 时间:", utcDate);
|
||||
//console.log("转换后的本地时间:", formattedDate);
|
||||
notify.info({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
@@ -69,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: () => '查看' })
|
||||
}
|
||||
@@ -79,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>
|
||||
@@ -112,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" />
|
||||
@@ -134,6 +185,8 @@ EventsOn("updateVersion",async (msg) => {
|
||||
</p>
|
||||
<p>
|
||||
感谢以下开发者:
|
||||
<a href="https://github.com/GiCo001" target="_blank">@Gico</a><n-divider vertical />
|
||||
<a href="https://github.com/CodeNoobLH" target="_blank">浓睡不消残酒</a><n-divider vertical />
|
||||
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
|
||||
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />
|
||||
<a href="https://github.com/2lovecode" target="_blank">@2lovecode</a><n-divider vertical />
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GetConfig,
|
||||
GetFollowedFund,
|
||||
GetfundList,
|
||||
GetVersionInfo,
|
||||
GetVersionInfo, OpenURL,
|
||||
UnFollowFund
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import vueDanmaku from 'vue3-danmaku'
|
||||
@@ -47,7 +47,7 @@ onBeforeMount(()=>{
|
||||
})
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -60,7 +60,7 @@ onMounted(() => {
|
||||
//ws.value = new WebSocket('ws://localhost:16688/ws'); // 替换为你的 WebSocket 服务器地址
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('WebSocket 连接已打开');
|
||||
//console.log('WebSocket 连接已打开');
|
||||
};
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
@@ -74,13 +74,13 @@ onMounted(() => {
|
||||
};
|
||||
|
||||
ws.value.onclose = () => {
|
||||
console.log('WebSocket 连接已关闭');
|
||||
//console.log('WebSocket 连接已关闭');
|
||||
};
|
||||
|
||||
ticker.value=setInterval(() => {
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
}, 1000*60)
|
||||
|
||||
@@ -103,7 +103,7 @@ function AddFund(){
|
||||
message.success("关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -114,7 +114,7 @@ function unFollow(code){
|
||||
message.success("取消关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
94
frontend/src/components/industryMoneyRank.vue
Normal file
94
frontend/src/components/industryMoneyRank.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
|
||||
import {CaretDown, CaretUp, RefreshCircleOutline} from "@vicons/ionicons5";
|
||||
import {NText,useMessage} from "naive-ui";
|
||||
import {onBeforeUnmount, onMounted, onUnmounted, ref} from "vue";
|
||||
import {GetIndustryMoneyRankSina} from "../../wailsjs/go/main/App";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
|
||||
const props = defineProps({
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '行业资金排名(净流入)'
|
||||
},
|
||||
fenlei: {
|
||||
type: String,
|
||||
default: '0'
|
||||
},
|
||||
sort: {
|
||||
type: String,
|
||||
default: 'netamount'
|
||||
},
|
||||
})
|
||||
const message = useMessage()
|
||||
const dataList= ref([])
|
||||
const sort = ref(props.sort)
|
||||
const fenlei= ref(props.fenlei)
|
||||
|
||||
const interval = ref(null)
|
||||
onMounted(()=>{
|
||||
sort.value=props.sort
|
||||
fenlei.value=props.fenlei
|
||||
GetRankData()
|
||||
interval.value=setInterval(()=>{
|
||||
GetRankData()
|
||||
},1000*60)
|
||||
})
|
||||
onBeforeUnmount(()=>{
|
||||
clearInterval(interval.value)
|
||||
})
|
||||
function GetRankData(){
|
||||
message.loading("正在刷新数据...")
|
||||
GetIndustryMoneyRankSina(fenlei.value,sort.value).then(result => {
|
||||
if(result.length>0){
|
||||
dataList.value = result
|
||||
//console.log(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-table striped size="small">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>板块名称</n-th>
|
||||
<n-th>涨跌幅</n-th>
|
||||
<n-th>流入资金/万</n-th>
|
||||
<n-th>流出资金/万</n-th>
|
||||
<n-th>净流入/万<n-icon v-if="sort==='0'" :component="CaretDown"/><n-icon v-if="sort==='1'" :component="CaretUp"/></n-th>
|
||||
<n-th>净流入率</n-th>
|
||||
<n-th>领涨股</n-th>
|
||||
<n-th>涨跌幅</n-th>
|
||||
<n-th>最新价</n-th>
|
||||
<n-th>净流入率</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in dataList" :key="item.category">
|
||||
<n-td><n-tag :bordered=false type="info">{{item.name}}</n-tag></n-td>
|
||||
<n-td> <n-text :type="item.avg_changeratio>0?'error':'success'">{{(item.avg_changeratio*100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td><n-text type="info">{{(item.inamount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text type="info">{{(item.outamount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text :type="item.netamount>0?'error':'success'">{{(item.netamount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text :type="item.ratioamount>0?'error':'success'">{{(item.ratioamount*100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td>
|
||||
<!-- <n-text type="info">{{item.ts_name}}</n-text>-->
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-button tag="a" text :type="item.ts_changeratio>0?'error':'success'" :bordered=false >{{ item.ts_name }}</n-button>
|
||||
</template>
|
||||
<k-line-chart style="width: 800px" :code="item.ts_symbol" :chart-height="500" :name="item.ts_name" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td><n-text :type="item.ts_changeratio>0?'error':'success'">{{(item.ts_changeratio*100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td><n-text type="info">{{item.ts_trade}}</n-text></n-td>
|
||||
<n-td><n-text :type="item.ts_ratioamount>0?'error':'success'">{{(item.ts_ratioamount*100).toFixed(2)}}%</n-text></n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
697
frontend/src/components/market.vue
Normal file
697
frontend/src/components/market.vue
Normal file
@@ -0,0 +1,697 @@
|
||||
<script setup>
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, ref} from 'vue'
|
||||
import {
|
||||
GetAIResponseResult,
|
||||
GetConfig,
|
||||
GetIndustryRank,
|
||||
GetPromptTemplates,
|
||||
GetTelegraphList,
|
||||
GlobalStockIndexes,
|
||||
ReFleshTelegraphList,
|
||||
SaveAIResponseResult,
|
||||
SaveAsMarkdown,
|
||||
ShareAnalysis,
|
||||
SummaryStockNews
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
|
||||
import NewsList from "./newsList.vue";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
import { CaretDown, CaretUp, PulseOutline,} from "@vicons/ionicons5";
|
||||
import {NAvatar, NButton, NFlex, NText, useMessage, useNotification} from "naive-ui";
|
||||
import {MdPreview} from "md-editor-v3";
|
||||
import {useRoute} from 'vue-router'
|
||||
import RankTable from "./rankTable.vue";
|
||||
import IndustryMoneyRank from "./industryMoneyRank.vue";
|
||||
import StockResearchReportList from "./StockResearchReportList.vue";
|
||||
import StockNoticeList from "./StockNoticeList.vue";
|
||||
import LongTigerRankList from "./LongTigerRankList.vue";
|
||||
import IndustryResearchReportList from "./IndustryResearchReportList.vue";
|
||||
import HotStockList from "./HotStockList.vue";
|
||||
import HotEvents from "./HotEvents.vue";
|
||||
import HotTopics from "./HotTopics.vue";
|
||||
import InvestCalendarTimeLine from "./InvestCalendarTimeLine.vue";
|
||||
import ClsCalendarTimeLine from "./ClsCalendarTimeLine.vue";
|
||||
import SelectStock from "./SelectStock.vue";
|
||||
import Stockhotmap from "./stockhotmap.vue";
|
||||
|
||||
const route = useRoute()
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
|
||||
const message = useMessage()
|
||||
const notify = useNotification()
|
||||
const panelHeight = ref(window.innerHeight - 240)
|
||||
|
||||
const telegraphList = ref([])
|
||||
const sinaNewsList = ref([])
|
||||
const common = ref([])
|
||||
const america = ref([])
|
||||
const europe = ref([])
|
||||
const asia = ref([])
|
||||
const other = ref([])
|
||||
const globalStockIndexes = ref(null)
|
||||
const summaryModal = ref(false)
|
||||
const summaryBTN = ref(true)
|
||||
const darkTheme = ref(false)
|
||||
const theme = computed(() => {
|
||||
return darkTheme ? 'dark' : 'light'
|
||||
})
|
||||
const aiSummary = ref(``)
|
||||
const aiSummaryTime = ref("")
|
||||
const modelName = ref("")
|
||||
const chatId = ref("")
|
||||
const question = ref(``)
|
||||
const sysPromptId = ref(0)
|
||||
const loading = ref(true)
|
||||
const sysPromptOptions = ref([])
|
||||
const userPromptOptions = ref([])
|
||||
const promptTemplates = ref([])
|
||||
const industryRanks = ref([])
|
||||
const sort = ref("0")
|
||||
const nowTab = ref("市场快讯")
|
||||
const indexInterval = ref(null)
|
||||
const indexIndustryRank = ref(null)
|
||||
const stockCode= ref('')
|
||||
const enableTools= ref(true)
|
||||
|
||||
function getIndex() {
|
||||
GlobalStockIndexes().then((res) => {
|
||||
globalStockIndexes.value = res
|
||||
common.value = res["common"]
|
||||
america.value = res["america"]
|
||||
europe.value = res["europe"]
|
||||
asia.value = res["asia"]
|
||||
other.value = res["other"]
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
nowTab.value = route.query.name
|
||||
stockCode.value = route.query.stockCode
|
||||
GetConfig().then(result => {
|
||||
summaryBTN.value = result.openAiEnable
|
||||
darkTheme.value = result.darkTheme
|
||||
})
|
||||
GetPromptTemplates("", "").then(res => {
|
||||
promptTemplates.value = res
|
||||
sysPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型系统Prompt')
|
||||
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
|
||||
})
|
||||
|
||||
GetTelegraphList("财联社电报").then((res) => {
|
||||
telegraphList.value = res
|
||||
})
|
||||
GetTelegraphList("新浪财经").then((res) => {
|
||||
sinaNewsList.value = res
|
||||
})
|
||||
getIndex();
|
||||
industryRank();
|
||||
indexInterval.value = setInterval(() => {
|
||||
getIndex()
|
||||
}, 3000)
|
||||
|
||||
indexIndustryRank.value = setInterval(() => {
|
||||
industryRank()
|
||||
}, 1000 * 10)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("changeMarketTab")
|
||||
EventsOff("newTelegraph")
|
||||
EventsOff("newSinaNews")
|
||||
EventsOff("summaryStockNews")
|
||||
clearInterval(indexInterval.value)
|
||||
clearInterval(indexIndustryRank.value)
|
||||
})
|
||||
|
||||
EventsOn("changeMarketTab", async (msg) => {
|
||||
//message.info(msg.name)
|
||||
updateTab(msg.name)
|
||||
})
|
||||
|
||||
EventsOn("newTelegraph", (data) => {
|
||||
if (data!=null) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
telegraphList.value.pop()
|
||||
}
|
||||
telegraphList.value.unshift(...data)
|
||||
}
|
||||
})
|
||||
EventsOn("newSinaNews", (data) => {
|
||||
if (data!=null) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
sinaNewsList.value.pop()
|
||||
}
|
||||
sinaNewsList.value.unshift(...data)
|
||||
}
|
||||
})
|
||||
|
||||
//获取页面高度
|
||||
window.onresize = () => {
|
||||
panelHeight.value = window.innerHeight - 240
|
||||
}
|
||||
|
||||
function getAreaName(code) {
|
||||
switch (code) {
|
||||
case "america":
|
||||
return "美洲"
|
||||
case "europe":
|
||||
return "欧洲"
|
||||
case "asia":
|
||||
return "亚洲"
|
||||
case "common":
|
||||
return "常用"
|
||||
case "other":
|
||||
return "其他"
|
||||
}
|
||||
}
|
||||
|
||||
function changeIndustryRankSort() {
|
||||
if (sort.value === "0") {
|
||||
sort.value = "1"
|
||||
} else {
|
||||
sort.value = "0"
|
||||
}
|
||||
industryRank()
|
||||
}
|
||||
|
||||
function industryRank() {
|
||||
|
||||
GetIndustryRank(sort.value, 150).then(result => {
|
||||
if (result.length > 0) {
|
||||
//console.log(result)
|
||||
industryRanks.value = result
|
||||
} else {
|
||||
message.info("暂无数据")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function reAiSummary() {
|
||||
aiSummary.value = ""
|
||||
summaryModal.value = true
|
||||
loading.value = true
|
||||
SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
|
||||
}
|
||||
|
||||
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
|
||||
loading.value = false
|
||||
|
||||
const date = new Date(result.CreatedAt);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
aiSummaryTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
modelName.value = result.modelName
|
||||
} else {
|
||||
aiSummaryTime.value = ""
|
||||
aiSummary.value = ""
|
||||
modelName.value = ""
|
||||
//SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateTab(name) {
|
||||
summaryBTN.value = (name === "市场快讯");
|
||||
nowTab.value = name
|
||||
}
|
||||
|
||||
EventsOn("summaryStockNews", async (msg) => {
|
||||
loading.value = false
|
||||
////console.log(msg)
|
||||
if (msg === "DONE") {
|
||||
SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value)
|
||||
message.info("AI分析完成!")
|
||||
message.destroyAll()
|
||||
|
||||
} else {
|
||||
if (msg.chatId) {
|
||||
chatId.value = msg.chatId
|
||||
}
|
||||
if (msg.question) {
|
||||
question.value = msg.question
|
||||
}
|
||||
if (msg.content) {
|
||||
aiSummary.value = aiSummary.value + msg.content
|
||||
}
|
||||
if (msg.extraContent) {
|
||||
aiSummary.value = aiSummary.value + msg.extraContent
|
||||
}
|
||||
if (msg.model) {
|
||||
modelName.value = msg.model
|
||||
}
|
||||
if (msg.time) {
|
||||
aiSummaryTime.value = msg.time
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function copyToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(aiSummary.value);
|
||||
message.success('分析结果已复制到剪切板');
|
||||
} catch (err) {
|
||||
message.error('复制失败: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
function saveAsMarkdown() {
|
||||
SaveAsMarkdown('市场资讯', '市场资讯').then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
}
|
||||
|
||||
function share() {
|
||||
ShareAnalysis('市场资讯', '市场资讯').then(msg => {
|
||||
//message.info(msg)
|
||||
notify.info({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: false,
|
||||
src: icon.value
|
||||
}),
|
||||
title: '分享到社区',
|
||||
duration: 1000 * 30,
|
||||
content: () => {
|
||||
return h('div', {
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
'font-size': '14px',
|
||||
}
|
||||
}, {default: () => msg})
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function ReFlesh(source) {
|
||||
//console.log("ReFlesh:", source)
|
||||
ReFleshTelegraphList(source).then(res => {
|
||||
if (source === "财联社电报") {
|
||||
telegraphList.value = res
|
||||
}
|
||||
if (source === "新浪财经") {
|
||||
sinaNewsList.value = res
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<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>
|
||||
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="全球股指" tab="全球股指">
|
||||
<n-tabs type="segment" animated>
|
||||
<n-tab-pane name="全球指数" tab="全球指数">
|
||||
<n-grid :cols="5" :y-gap="0">
|
||||
<n-gi v-for="(val, key) in globalStockIndexes" :key="key">
|
||||
<n-list bordered>
|
||||
<template #header>
|
||||
{{ getAreaName(key) }}
|
||||
</template>
|
||||
<n-list-item v-for="item in val" :key="item.code">
|
||||
<n-grid :cols="3" :y-gap="0">
|
||||
<n-gi>
|
||||
|
||||
<n-text :type="item.zdf>0?'error':'success'">
|
||||
<n-image :src="item.img" width="20"/> {{ item.name }}
|
||||
</n-text>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-text :type="item.zdf>0?'error':'success'">{{ item.zxj }}</n-text>
|
||||
<n-text :type="item.zdf>0?'error':'success'">
|
||||
<n-number-animation :precision="2" :from="0" :to="item.zdf"/>
|
||||
%
|
||||
</n-text>
|
||||
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-text :type="item.state === 'open' ? 'success' : 'warning'">{{
|
||||
item.state === 'open' ? '开市' : '休市'
|
||||
}}
|
||||
</n-text>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="上证指数" tab="上证指数">
|
||||
<k-line-chart code="sh000001" :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="sz399001" :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="sz399006" :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="hkHSI" :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="us.IXIC" :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="us.DJI" :chart-height="panelHeight" name="道琼斯" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="标普500" tab="标普500">
|
||||
<k-line-chart code="us.INX" :chart-height="panelHeight" name="标普500" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="重大指数" tab="重大指数">
|
||||
<n-tabs type="segment" animated>
|
||||
<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">
|
||||
<k-line-chart code="sh000300" :chart-height="panelHeight" name="沪深300" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="上证50" tab="上证50">
|
||||
<k-line-chart code="sh000016" :chart-height="panelHeight" name="上证50" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证A500" tab="中证A500">
|
||||
<k-line-chart code="sh000510" :chart-height="panelHeight" name="中证A500" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证1000" tab="中证1000">
|
||||
<k-line-chart code="sh000852" :chart-height="panelHeight" name="中证1000" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证白酒" tab="中证白酒">
|
||||
<k-line-chart code="sz399997" :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="usYINN.AM" :chart-height="panelHeight" name="富时中国三倍做多" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="VIX恐慌指数" tab="VIX恐慌指数">
|
||||
<k-line-chart code="usUVXY.AM" :chart-height="panelHeight" name="VIX恐慌指数" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="行业排名" tab="行业排名">
|
||||
<n-tabs type="card" animated>
|
||||
<n-tab-pane name="行业涨幅排名" tab="行业涨幅排名">
|
||||
<n-table striped>
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>行业名称</n-th>
|
||||
<n-th @click="changeIndustryRankSort">行业涨幅
|
||||
<n-icon v-if="sort==='0'" :component="CaretDown"/>
|
||||
<n-icon v-if="sort==='1'" :component="CaretUp"/>
|
||||
</n-th>
|
||||
<n-th>行业5日涨幅</n-th>
|
||||
<n-th>行业20日涨幅</n-th>
|
||||
<n-th>领涨股</n-th>
|
||||
<n-th>涨幅</n-th>
|
||||
<n-th>最新价</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in industryRanks" :key="item.bd_code">
|
||||
<n-td>
|
||||
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
|
||||
<n-text type="info">{{ item.nzg_code }}</n-text>
|
||||
</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
<n-table striped>
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>行业名称</n-th>
|
||||
<n-th @click="changeIndustryRankSort">行业涨幅
|
||||
<n-icon v-if="sort==='0'" :component="CaretDown"/>
|
||||
<n-icon v-if="sort==='1'" :component="CaretUp"/>
|
||||
</n-th>
|
||||
<n-th>行业5日涨幅</n-th>
|
||||
<n-th>行业20日涨幅</n-th>
|
||||
<n-th>领涨股</n-th>
|
||||
<n-th>涨幅</n-th>
|
||||
<n-th>最新价</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in industryRanks" :key="item.bd_code">
|
||||
<n-td>
|
||||
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
|
||||
<n-text type="info">{{ item.nzg_code }}</n-text>
|
||||
</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="行业资金排名(净流入)" tab="行业资金排名">
|
||||
<industryMoneyRank :fenlei="'0'" :header-title="'行业资金排名(净流入)'" :sort="'netamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="证监会行业资金排名(净流入)" tab="证监会行业资金排名">
|
||||
<industryMoneyRank :fenlei="'2'" :header-title="'证监会行业资金排名(净流入)'" :sort="'netamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="概念板块资金排名(净流入)" tab="概念板块资金排名">
|
||||
<industryMoneyRank :fenlei="'1'" :header-title="'概念板块资金排名(净流入)'" :sort="'netamount'"/>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="个股资金流向" tab="个股资金流向">
|
||||
<n-tabs type="card" animated>
|
||||
<n-tab-pane name="netamount" tab="净流入额排名">
|
||||
<RankTable :header-title="'净流入额排名'" :sort="'netamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="outamount" tab="流出资金排名">
|
||||
<RankTable :header-title="'流出资金排名'" :sort="'outamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="ratioamount" tab="净流入率排名">
|
||||
<RankTable :header-title="'净流入率排名'" :sort="'ratioamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r0_net" tab="主力净流入额排名">
|
||||
<RankTable :header-title="'主力净流入额排名'" :sort="'r0_net'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r0_out" tab="主力流出排名">
|
||||
<RankTable :header-title="'主力流出排名'" :sort="'r0_out'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r0_ratio" tab="主力净流入率排名">
|
||||
<RankTable :header-title="'主力净流入率排名'" :sort="'r0_ratio'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r3_net" tab="散户净流入额排名">
|
||||
<RankTable :header-title="'散户净流入额排名'" :sort="'r3_net'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r3_out" tab="散户流出排名">
|
||||
<RankTable :header-title="'散户流出排名'" :sort="'r3_out'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r3_ratio" tab="散户净流入率排名">
|
||||
<RankTable :header-title="'散户净流入率排名'" :sort="'r3_ratio'"/>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="龙虎榜" tab="龙虎榜">
|
||||
<LongTigerRankList />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="个股研报" tab="个股研报">
|
||||
<StockResearchReportList :stock-code="stockCode"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="公司公告" tab="公司公告 ">
|
||||
<StockNoticeList :stock-code="stockCode" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="行业研究" tab="行业研究 ">
|
||||
<IndustryResearchReportList/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="当前热门" tab="当前热门">
|
||||
<n-tabs type="card" animated>
|
||||
<n-tab-pane name="全球" tab="全球">
|
||||
<HotStockList :market-type="'10'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="沪深" tab="沪深">
|
||||
<HotStockList :market-type="'12'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="港股" tab="港股">
|
||||
<HotStockList :market-type="'13'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="美股" tab="美股">
|
||||
<HotStockList :market-type="'11'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="热门话题" tab="热门话题">
|
||||
<n-grid :cols="1" :y-gap="10">
|
||||
<n-grid-item>
|
||||
<HotTopics/>
|
||||
</n-grid-item>
|
||||
<!-- <n-grid-item>-->
|
||||
<!-- <HotEvents/>-->
|
||||
<!-- </n-grid-item>-->
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="重大事件时间轴" tab="重大事件时间轴">
|
||||
<InvestCalendarTimeLine />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="财经日历" tab="财经日历">
|
||||
<ClsCalendarTimeLine />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="指标选股" tab="指标选股">
|
||||
<select-stock />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="名站优选" tab="名站优选">
|
||||
<Stockhotmap />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
<n-modal transform-origin="center" v-model:show="summaryModal" preset="card" style="width: 800px;"
|
||||
:title="'AI市场资讯总结'">
|
||||
<n-spin size="small" :show="loading">
|
||||
<MdPreview style="height: 440px;text-align: left" :modelValue="aiSummary" :theme="theme"/>
|
||||
</n-spin>
|
||||
<template #footer>
|
||||
<n-flex justify="space-between" ref="tipsRef">
|
||||
<n-text type="info" v-if="aiSummaryTime">
|
||||
<n-tag v-if="modelName" type="warning" round :title="chatId" :bordered="false">{{ modelName }}</n-tag>
|
||||
{{ aiSummaryTime }}
|
||||
</n-text>
|
||||
<n-text type="error">*AI分析结果仅供参考,请以实际行情为准。投资需谨慎,风险自担。</n-text>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #action>
|
||||
<n-flex justify="left" style="margin-bottom: 10px">
|
||||
<n-switch v-model:value="enableTools" :round="false">
|
||||
<template #checked>
|
||||
启用AI函数工具调用
|
||||
</template>
|
||||
<template #unchecked>
|
||||
不启用AI函数工具调用
|
||||
</template>
|
||||
</n-switch>
|
||||
<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="sysPromptId" label-field="name" value-field="ID"
|
||||
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
|
||||
<n-select style="width: 49%" v-model:value="question" label-field="name" value-field="content"
|
||||
:options="userPromptOptions" placeholder="请选择用户提示词"/>
|
||||
</n-flex>
|
||||
<n-flex justify="right">
|
||||
<n-input v-model:value="question" style="text-align: left" clearable
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入您的问题:例如 总结和分析股票市场新闻中的投资机会"
|
||||
:autosize="{
|
||||
minRows: 2,
|
||||
maxRows: 5
|
||||
}"
|
||||
/>
|
||||
<n-button size="tiny" type="warning" @click="reAiSummary">再次总结</n-button>
|
||||
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
|
||||
<n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button>
|
||||
<n-button size="tiny" type="error" @click="share">分享到项目社区</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<div style="position: fixed;bottom: 18px;right:25px;z-index: 10;" v-if="summaryBTN">
|
||||
<n-input-group>
|
||||
<n-button type="primary" @click="getAiSummary">
|
||||
<n-icon :component="PulseOutline"/> AI总结
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
</style>
|
||||
374
frontend/src/components/moneyTrend.vue
Normal file
374
frontend/src/components/moneyTrend.vue
Normal file
@@ -0,0 +1,374 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from "vue";
|
||||
import {GetStockMoneyTrendByDay} from "../../wailsjs/go/main/App";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const {code, name, darkTheme, days, chartHeight} = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
days: {
|
||||
type: Number,
|
||||
default: 14
|
||||
},
|
||||
chartHeight: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
darkTheme: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const LineChartRef = ref(null);
|
||||
|
||||
onMounted(
|
||||
() => {
|
||||
handleLine(code, days)
|
||||
}
|
||||
)
|
||||
const handleLine = (code, days) => {
|
||||
GetStockMoneyTrendByDay(code, days).then(result => {
|
||||
//console.log("GetStockMoneyTrendByDay", result)
|
||||
const chart = echarts.init(LineChartRef.value);
|
||||
const categoryData = [];
|
||||
const netamount_values = [];
|
||||
const r0_net_values = [];
|
||||
const trades_values = [];
|
||||
let volume = []
|
||||
|
||||
let min = 0
|
||||
let max = 0
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
let resultElement = result[i]
|
||||
categoryData.push(resultElement.opendate)
|
||||
let netamount = (resultElement.netamount / 10000).toFixed(2);
|
||||
netamount_values.push(netamount)
|
||||
let price = Number(resultElement.trade);
|
||||
trades_values.push(price)
|
||||
r0_net_values.push((resultElement.r0_net / 10000).toFixed(2))
|
||||
|
||||
if (min === 0 || min > price) {
|
||||
min = price
|
||||
}
|
||||
if (max < price) {
|
||||
max = price
|
||||
}
|
||||
|
||||
if (i > 0) {
|
||||
let b = Number(Number(result[i].netamount) + Number(result[i - 1].netamount)) / 10000
|
||||
volume.push(b.toFixed(2))
|
||||
} else {
|
||||
volume.push((Number(result[i].netamount) / 10000).toFixed(2))
|
||||
}
|
||||
|
||||
}
|
||||
//console.log("volume", volume)
|
||||
const upColor = '#ec0000';
|
||||
const downColor = '#00da3c';
|
||||
let option = {
|
||||
title: {
|
||||
text: name,
|
||||
left: '20px',
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
lineStyle: {
|
||||
color: '#376df4',
|
||||
width: 1,
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
borderWidth: 2,
|
||||
borderColor: darkTheme?'#456':'#ccc',
|
||||
backgroundColor: darkTheme?'#456':'#fff',
|
||||
padding: 10,
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
},
|
||||
},
|
||||
axisPointer: {
|
||||
link: [
|
||||
{
|
||||
xAxisIndex: 'all'
|
||||
}
|
||||
],
|
||||
label: {
|
||||
backgroundColor: '#888'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
data: ['当日净流入', '主力当日净流入','累计净流入', '股价'],
|
||||
selected: {
|
||||
'当日净流入': true,
|
||||
'主力当日净流入': true,
|
||||
'累计净流入': true,
|
||||
'股价': true,
|
||||
},
|
||||
//orient: 'vertical',
|
||||
textStyle: {
|
||||
color: darkTheme ? 'rgb(253,252,252)' : '#456'
|
||||
},
|
||||
right: 150,
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0, 1],
|
||||
start: 86,
|
||||
end: 100
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
xAxisIndex: [0, 1],
|
||||
type: 'slider',
|
||||
top: '90%',
|
||||
start: 86,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
grid: [
|
||||
{
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
height: '50%',
|
||||
},
|
||||
{
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
top: '74%',
|
||||
height: '15%'
|
||||
},
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: categoryData,
|
||||
axisPointer: {
|
||||
z: 100
|
||||
},
|
||||
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: false },
|
||||
splitLine: { show: false },
|
||||
min: 'dataMin',
|
||||
max: 'dataMax',
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
gridIndex: 1,
|
||||
type: 'category',
|
||||
data: categoryData,
|
||||
axisLabel: {
|
||||
show: false
|
||||
},
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
name: '当日净流入/万',
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '股价',
|
||||
type: 'value',
|
||||
min: min - 1,
|
||||
max: max + 1,
|
||||
minInterval: 0.01,
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
},
|
||||
{
|
||||
gridIndex: 1,
|
||||
name: '累计净流入/万',
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
yAxisIndex: 0,
|
||||
name: '当日净流入',
|
||||
data: netamount_values,
|
||||
smooth: false,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
markPoint: {
|
||||
symbol: 'arrow',
|
||||
symbolRotate: 90,
|
||||
symbolSize: [10, 20],
|
||||
symbolOffset: [10, 0],
|
||||
itemStyle: {
|
||||
color: '#0d7dfc'
|
||||
},
|
||||
label: {
|
||||
position: 'right',
|
||||
},
|
||||
data: [
|
||||
{type: 'max', name: 'Max'},
|
||||
{type: 'min', name: 'Min'}
|
||||
]
|
||||
},
|
||||
markLine: {
|
||||
data: [
|
||||
{
|
||||
type: 'average',
|
||||
name: 'Average',
|
||||
lineStyle: {
|
||||
color: '#0077ff',
|
||||
width: 0.5
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
type: 'line'
|
||||
},
|
||||
{
|
||||
yAxisIndex: 0,
|
||||
name: '主力当日净流入',
|
||||
data: r0_net_values,
|
||||
smooth: false,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
// markPoint: {
|
||||
// symbol: 'arrow',
|
||||
// symbolRotate: 90,
|
||||
// symbolSize: [10, 20],
|
||||
// symbolOffset: [10, 0],
|
||||
// itemStyle: {
|
||||
// color: '#0d7dfc'
|
||||
// },
|
||||
// label: {
|
||||
// position: 'right',
|
||||
// },
|
||||
// data: [
|
||||
// {type: 'max', name: 'Max'},
|
||||
// {type: 'min', name: 'Min'}
|
||||
// ]
|
||||
// },
|
||||
// markLine: {
|
||||
// data: [
|
||||
// {
|
||||
// type: 'average',
|
||||
// name: 'Average',
|
||||
// lineStyle: {
|
||||
// color: '#0077ff',
|
||||
// width: 0.5
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
// },
|
||||
type: 'bar'
|
||||
},
|
||||
{
|
||||
yAxisIndex: 1,
|
||||
name: '股价',
|
||||
type: 'line',
|
||||
data: trades_values,
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 3
|
||||
},
|
||||
markPoint: {
|
||||
symbol: 'arrow',
|
||||
symbolRotate: 90,
|
||||
symbolSize: [10, 20],
|
||||
symbolOffset: [10, 0],
|
||||
itemStyle: {
|
||||
color: '#f39509'
|
||||
},
|
||||
label: {
|
||||
position: 'right',
|
||||
},
|
||||
data: [
|
||||
{type: 'max', name: 'Max'},
|
||||
{type: 'min', name: 'Min'}
|
||||
]
|
||||
},
|
||||
markLine: {
|
||||
data: [
|
||||
{
|
||||
type: 'average',
|
||||
name: 'Average',
|
||||
lineStyle: {
|
||||
color: '#f39509',
|
||||
width: 0.5
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 2,
|
||||
name: '累计净流入',
|
||||
data: volume,
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
markPoint: {
|
||||
symbol: 'arrow',
|
||||
symbolRotate: 90,
|
||||
symbolSize: [10, 20],
|
||||
symbolOffset: [10, 0],
|
||||
// itemStyle: {
|
||||
// color: '#f39509'
|
||||
// },
|
||||
label: {
|
||||
position: 'right',
|
||||
},
|
||||
data: [
|
||||
{type: 'max', name: 'Max'},
|
||||
{type: 'min', name: 'Min'}
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
};
|
||||
chart.setOption(option);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="LineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
58
frontend/src/components/newsList.vue
Normal file
58
frontend/src/components/newsList.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import {ReFleshTelegraphList} from "../../wailsjs/go/main/App";
|
||||
import {RefreshCircle, RefreshCircleSharp, RefreshOutline} from "@vicons/ionicons5";
|
||||
|
||||
const { headerTitle,newsList } = defineProps({
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '市场资讯'
|
||||
},
|
||||
newsList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:message'])
|
||||
|
||||
const updateMessage = () => {
|
||||
emits('update:message', headerTitle)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-list bordered>
|
||||
<template #header>
|
||||
<n-flex justify="space-between">
|
||||
<n-tag :bordered="false" size="large" type="success" >{{ headerTitle }}</n-tag>
|
||||
<n-button :bordered="false" @click="updateMessage"><n-icon color="#409EFF" size="25" :component="RefreshCircleSharp"/></n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-list-item v-for="item in newsList">
|
||||
<n-space justify="start">
|
||||
<n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'">
|
||||
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>
|
||||
{{ item.content }}
|
||||
</n-text>
|
||||
</n-space>
|
||||
<n-space v-if="item.subjects" style="margin-top: 2px">
|
||||
<n-tag :bordered="false" type="success" size="small" v-for="sub in item.subjects">
|
||||
{{ sub }}
|
||||
</n-tag>
|
||||
<n-space v-if="item.stocks">
|
||||
<n-tag :bordered="false" type="warning" size="small" v-for="sub in item.stocks">
|
||||
{{ sub }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<n-tag v-if="item.url" :bordered="false" type="warning" size="small">
|
||||
<a :href="item.url" target="_blank">
|
||||
<n-text type="warning">查看原文</n-text>
|
||||
</a>
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
101
frontend/src/components/rankTable.vue
Normal file
101
frontend/src/components/rankTable.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup>
|
||||
|
||||
import {CaretDown, CaretUp, RefreshCircleOutline} from "@vicons/ionicons5";
|
||||
import {NText,useMessage} from "naive-ui";
|
||||
import {onBeforeUnmount, onMounted, onUnmounted, ref} from "vue";
|
||||
import {GetMoneyRankSina} from "../../wailsjs/go/main/App";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
|
||||
const props = defineProps({
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '净流入额排名'
|
||||
},
|
||||
sort: {
|
||||
type: String,
|
||||
default: 'netamount'
|
||||
},
|
||||
})
|
||||
const message = useMessage()
|
||||
const dataList= ref([])
|
||||
const sort = ref(props.sort)
|
||||
const interval = ref(null)
|
||||
onMounted(()=>{
|
||||
sort.value=props.sort
|
||||
GetMoneyRankSinaData()
|
||||
interval.value=setInterval(()=>{
|
||||
GetMoneyRankSinaData()
|
||||
},1000*60)
|
||||
})
|
||||
onBeforeUnmount(()=>{
|
||||
clearInterval(interval.value)
|
||||
})
|
||||
function GetMoneyRankSinaData(){
|
||||
message.loading("正在刷新数据...")
|
||||
GetMoneyRankSina(sort.value).then(result => {
|
||||
if(result.length>0){
|
||||
dataList.value = result
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-table striped size="small">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>代码</n-th>
|
||||
<n-th>名称</n-th>
|
||||
<n-th>最新价</n-th>
|
||||
<n-th>涨跌幅</n-th>
|
||||
<n-th>换手率</n-th>
|
||||
<n-th>成交额/万</n-th>
|
||||
<n-th>流出资金/万</n-th>
|
||||
<n-th>流入资金/万</n-th>
|
||||
<n-th>净流入/万</n-th>
|
||||
<n-th>净流入率</n-th>
|
||||
<n-th v-if="sort === 'r0_net'||sort==='r0_out'">主力流出/万</n-th>
|
||||
<n-th v-if="sort === 'r0_net'">主力流入/万</n-th>
|
||||
<n-th v-if="sort === 'r0_net'">主力净流入/万</n-th>
|
||||
<n-th >主力净流入率</n-th>
|
||||
<n-th v-if="sort === 'r3_net'||sort==='r3_out'">散户流出/万</n-th>
|
||||
<n-th v-if="sort === 'r3_net'">散户流入/万</n-th>
|
||||
<n-th v-if="sort === 'r3_net'">散户净流入/万</n-th>
|
||||
<n-th >散户净流入率</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in dataList" :key="item.symbol">
|
||||
<n-td><n-tag :bordered=false type="info">{{ item.symbol }}</n-tag></n-td>
|
||||
<n-td>
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-button tag="a" text :type="item.changeratio>0?'error':'success'" :bordered=false >{{ item.name }}</n-button>
|
||||
</template>
|
||||
<k-line-chart style="width: 800px" :code="item.symbol" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{item.trade}}</n-text></n-td>
|
||||
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{(item.changeratio*100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td><n-text :type="item.turnover>500?'error':'info'">{{(item.turnover/100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td><n-text type="info">{{(item.amount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text type="info"> {{(item.outamount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text type="info"> {{(item.inamount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text type="info"> {{(item.netamount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text :type="item.ratioamount>0?'error':'success'"> {{(item.ratioamount*100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td v-if="sort === 'r0_net'||sort==='r0_out'"><n-text type="success"> {{(item.r0_out/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td v-if="sort === 'r0_net'"><n-text type="error"> {{(item.r0_in/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td v-if="sort === 'r0_net'"><n-text :type="item.r0_net>0?'error':'success'"> {{(item.r0_net/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td ><n-text :type="item.r0_ratio>0?'error':'success'"> {{(item.r0_ratio*100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td v-if="sort === 'r3_net'||sort==='r3_out'"><n-text type="success"> {{(item.r3_out/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td v-if="sort === 'r3_net'"><n-text type="error"> {{(item.r3_in/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td v-if="sort === 'r3_net'"><n-text :type="item.r3_net>0?'error':'success'"> {{(item.r3_net/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td ><n-text :type="item.r3_ratio>0?'error':'success'"> {{(item.r3_ratio*100).toFixed(2)}}%</n-text></n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -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()
|
||||
@@ -44,6 +44,9 @@ const formValue = ref({
|
||||
browserPath: '',
|
||||
enableNews:false,
|
||||
darkTheme:true,
|
||||
enableFund:false,
|
||||
enablePushNews:false,
|
||||
sponsorCode:"",
|
||||
})
|
||||
const promptTemplates=ref([])
|
||||
onMounted(()=>{
|
||||
@@ -76,13 +79,17 @@ onMounted(()=>{
|
||||
formValue.value.browserPath = res.browserPath
|
||||
formValue.value.enableNews = res.enableNews
|
||||
formValue.value.darkTheme = res.darkTheme
|
||||
formValue.value.enableFund = res.enableFund
|
||||
formValue.value.enablePushNews = res.enablePushNews
|
||||
formValue.value.sponsorCode = res.sponsorCode
|
||||
|
||||
console.log(res)
|
||||
|
||||
//console.log(res)
|
||||
})
|
||||
//message.info("加载完成")
|
||||
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
console.log(res)
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
})
|
||||
@@ -114,15 +121,30 @@ function saveConfig(){
|
||||
enableDanmu:formValue.value.enableDanmu,
|
||||
browserPath:formValue.value.browserPath,
|
||||
enableNews:formValue.value.enableNews,
|
||||
darkTheme:formValue.value.darkTheme
|
||||
darkTheme:formValue.value.darkTheme,
|
||||
enableFund:formValue.value.enableFund,
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +184,7 @@ function importConfig(){
|
||||
let reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
let config = JSON.parse(e.target.result);
|
||||
console.log(config)
|
||||
//console.log(config)
|
||||
formValue.value.ID = config.ID
|
||||
formValue.value.tushareToken = config.tushareToken
|
||||
formValue.value.dingPush = {
|
||||
@@ -191,6 +213,9 @@ function importConfig(){
|
||||
formValue.value.browserPath = config.browserPath
|
||||
formValue.value.enableNews = config.enableNews
|
||||
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);
|
||||
@@ -200,7 +225,7 @@ function importConfig(){
|
||||
|
||||
|
||||
window.onerror = function (event, source, lineno, colno, error) {
|
||||
console.log(event, source, lineno, colno, error)
|
||||
//console.log(event, source, lineno, colno, error)
|
||||
// 将错误信息发送给后端
|
||||
EventsEmit("frontendError", {
|
||||
page: "settings.vue",
|
||||
@@ -233,14 +258,14 @@ function savePrompt(){
|
||||
AddPrompt(formPrompt.value).then(res=>{
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
console.log(res)
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
showManagePromptsModal.value=false
|
||||
})
|
||||
}
|
||||
function editPrompt(prompt){
|
||||
console.log(prompt)
|
||||
//console.log(prompt)
|
||||
formPrompt.value.ID=prompt.ID
|
||||
formPrompt.value.Name=prompt.name
|
||||
formPrompt.value.Content=prompt.content
|
||||
@@ -251,7 +276,7 @@ function deletePrompt(ID){
|
||||
DelPrompt(ID).then(res=>{
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
console.log(res)
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
})
|
||||
@@ -259,60 +284,78 @@ 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-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="primary" style="font-size: 25px;font-weight: bold">基础设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="10" label="Tushare api token:" path="tushareToken" >
|
||||
<!-- <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 Token:" path="tushareToken" >
|
||||
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
|
||||
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="数据刷新间隔:" path="refreshInterval" >
|
||||
<n-form-item-gi :span="4" label="数据刷新间隔:" path="refreshInterval" >
|
||||
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
|
||||
<template #suffix>
|
||||
秒
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="暗黑主题:" path="darkTheme" >
|
||||
<n-form-item-gi :span="6" label="暗黑主题:" path="darkTheme" >
|
||||
<n-switch v-model:value="formValue.darkTheme" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" label="浏览器路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器路径" v-model:value="formValue.browserPath" clearable />
|
||||
<n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-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="primary" style="font-size: 25px;font-weight: bold">通知设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" >
|
||||
<!-- <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>
|
||||
<n-form-item-gi :span="6" label="是否启用本地推送:" path="localPush.enable" >
|
||||
<n-form-item-gi :span="4" label="本地推送:" path="localPush.enable" >
|
||||
<n-switch v-model:value="formValue.localPush.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="弹幕功能:" path="enableDanmu" >
|
||||
<n-form-item-gi :span="4" label="弹幕功能:" path="enableDanmu" >
|
||||
<n-switch v-model:value="formValue.enableDanmu" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="是否显示滚动快讯(重启生效):" path="enableNews" >
|
||||
<n-form-item-gi :span="4" label="显示滚动快讯:" path="enableNews" >
|
||||
<n-switch v-model:value="formValue.enableNews" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="市场资讯提醒:" path="enablePushNews" >
|
||||
<n-switch v-model:value="formValue.enablePushNews" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
|
||||
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
|
||||
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-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="primary" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="3" label="是否启用AI诊股:" path="openAI.enable" >
|
||||
<!-- <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>
|
||||
<n-form-item-gi :span="9" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl" >
|
||||
@@ -336,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" >
|
||||
@@ -346,7 +389,7 @@ function deletePrompt(ID){
|
||||
placeholder="请输入系统prompt"
|
||||
:autosize="{
|
||||
minRows: 5,
|
||||
maxRows: 8
|
||||
maxRows: 6
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
@@ -357,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
|
||||
@@ -428,5 +481,9 @@ function deletePrompt(ID){
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.cardHeaderClass{
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
137
frontend/src/components/stockSparkLine.vue
Normal file
137
frontend/src/components/stockSparkLine.vue
Normal 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>
|
||||
36
frontend/src/components/stockhotmap.vue
Normal file
36
frontend/src/components/stockhotmap.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<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://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://996.ninja/" :height="'calc(100vh - 252px)'"/>-->
|
||||
<!-- </n-tab-pane>-->
|
||||
|
||||
|
||||
<n-tab-pane name="欢迎推荐更多有趣的财经网页" tab="欢迎推荐更多有趣的财经网页">
|
||||
</n-tab-pane>
|
||||
<!-- <n-tab-pane name="自在量化" tab="自在量化">-->
|
||||
<!-- <embedded-url url="https://quant.zizizaizai.com/home"/>-->
|
||||
<!-- </n-tab-pane>-->
|
||||
</n-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import naive from 'naive-ui'
|
||||
import App from './App.vue'
|
||||
import router from './router/router'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import {createMemoryHistory, createRouter, createWebHashHistory, createWebHistory} from 'vue-router'
|
||||
|
||||
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 marketView from "../components/market.vue";
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: stockView,name: 'stock' },
|
||||
{ path: '/', component: stockView,name: 'stock'},
|
||||
{ path: '/fund', component: fundView,name: 'fund' },
|
||||
{ path: '/settings', component: settingsView,name: 'settings' },
|
||||
{ path: '/about', component: about,name: 'about' },
|
||||
{ path: '/about', component: aboutView,name: 'about' },
|
||||
{ path: '/market', component: marketView,name: 'market' },
|
||||
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
//history: createWebHistory(),
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
|
||||
81
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file → Executable file
81
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file → Executable file
@@ -2,15 +2,30 @@
|
||||
// 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>;
|
||||
|
||||
export function AddGroup(arg1:data.Group):Promise<string>;
|
||||
|
||||
export function AddPrompt(arg1:models.Prompt):Promise<string>;
|
||||
|
||||
export function CheckUpdate():Promise<void>;
|
||||
export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
|
||||
|
||||
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
|
||||
|
||||
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>>;
|
||||
|
||||
export function DelPrompt(arg1:number):Promise<string>;
|
||||
|
||||
export function EMDictCode(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function ExportConfig():Promise<string>;
|
||||
|
||||
export function Follow(arg1:string):Promise<string>;
|
||||
@@ -21,26 +36,80 @@ export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult
|
||||
|
||||
export function GetConfig():Promise<data.Settings>;
|
||||
|
||||
export function GetFollowList():Promise<any>;
|
||||
export function GetFollowList(arg1:number):Promise<any>;
|
||||
|
||||
export function GetFollowedFund():Promise<Array<data.FollowedFund>>;
|
||||
|
||||
export function GetGroupList():Promise<Array<data.Group>>;
|
||||
|
||||
export function GetGroupStockList(arg1:number):Promise<Array<data.GroupStock>>;
|
||||
|
||||
export function GetHotStrategy():Promise<Record<string, any>>;
|
||||
|
||||
export function GetIndustryMoneyRankSina(arg1:string,arg2:string):Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function GetIndustryRank(arg1:string,arg2:number):Promise<Array<any>>;
|
||||
|
||||
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>;
|
||||
|
||||
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
|
||||
|
||||
export function GetStockMinutePriceLineData(arg1:string,arg2:string):Promise<Record<string, any>>;
|
||||
|
||||
export function GetStockMoneyTrendByDay(arg1:string,arg2:number):Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function GetTelegraphList(arg1:string):Promise<any>;
|
||||
|
||||
export function GetVersionInfo():Promise<models.VersionInfo>;
|
||||
|
||||
export function GetfundList(arg1:string):Promise<Array<data.FundBasic>>;
|
||||
|
||||
export function GlobalStockIndexes():Promise<Record<string, any>>;
|
||||
|
||||
export function Greet(arg1:string):Promise<data.StockInfo>;
|
||||
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
|
||||
export function HotEvent(arg1:number):Promise<any>;
|
||||
|
||||
export function HotStock(arg1:string):Promise<any>;
|
||||
|
||||
export function HotTopic(arg1:number):Promise<Array<any>>;
|
||||
|
||||
export function IndustryResearchReport(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function LongTigerRank(arg1:string):Promise<any>;
|
||||
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any,arg5:boolean):Promise<void>;
|
||||
|
||||
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>;
|
||||
|
||||
export function RemoveStockGroup(arg1:string,arg2:string,arg3:number):Promise<string>;
|
||||
|
||||
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
|
||||
|
||||
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
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>;
|
||||
|
||||
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;
|
||||
@@ -55,6 +124,12 @@ export function SetStockSort(arg1:number,arg2:string):Promise<void>;
|
||||
|
||||
export function ShareAnalysis(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function StockNotice(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function StockResearchReport(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function SummaryStockNews(arg1:string,arg2:any,arg3:boolean):Promise<void>;
|
||||
|
||||
export function UnFollow(arg1:string):Promise<string>;
|
||||
|
||||
export function UnFollowFund(arg1:string):Promise<string>;
|
||||
|
||||
160
frontend/wailsjs/go/main/App.js
Normal file → Executable file
160
frontend/wailsjs/go/main/App.js
Normal file → Executable file
@@ -6,18 +6,46 @@ export function AddCronTask(arg1) {
|
||||
return window['go']['main']['App']['AddCronTask'](arg1);
|
||||
}
|
||||
|
||||
export function AddGroup(arg1) {
|
||||
return window['go']['main']['App']['AddGroup'](arg1);
|
||||
}
|
||||
|
||||
export function AddPrompt(arg1) {
|
||||
return window['go']['main']['App']['AddPrompt'](arg1);
|
||||
}
|
||||
|
||||
export function CheckUpdate() {
|
||||
return window['go']['main']['App']['CheckUpdate']();
|
||||
export function AddStockGroup(arg1, arg2) {
|
||||
return window['go']['main']['App']['AddStockGroup'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function AnalyzeSentiment(arg1) {
|
||||
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
|
||||
}
|
||||
|
||||
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() {
|
||||
return window['go']['main']['App']['ClsCalendar']();
|
||||
}
|
||||
|
||||
export function DelPrompt(arg1) {
|
||||
return window['go']['main']['App']['DelPrompt'](arg1);
|
||||
}
|
||||
|
||||
export function EMDictCode(arg1) {
|
||||
return window['go']['main']['App']['EMDictCode'](arg1);
|
||||
}
|
||||
|
||||
export function ExportConfig() {
|
||||
return window['go']['main']['App']['ExportConfig']();
|
||||
}
|
||||
@@ -38,22 +66,70 @@ export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
|
||||
export function GetFollowList() {
|
||||
return window['go']['main']['App']['GetFollowList']();
|
||||
export function GetFollowList(arg1) {
|
||||
return window['go']['main']['App']['GetFollowList'](arg1);
|
||||
}
|
||||
|
||||
export function GetFollowedFund() {
|
||||
return window['go']['main']['App']['GetFollowedFund']();
|
||||
}
|
||||
|
||||
export function GetGroupList() {
|
||||
return window['go']['main']['App']['GetGroupList']();
|
||||
}
|
||||
|
||||
export function GetGroupStockList(arg1) {
|
||||
return window['go']['main']['App']['GetGroupStockList'](arg1);
|
||||
}
|
||||
|
||||
export function GetHotStrategy() {
|
||||
return window['go']['main']['App']['GetHotStrategy']();
|
||||
}
|
||||
|
||||
export function GetIndustryMoneyRankSina(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetIndustryMoneyRankSina'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetIndustryRank(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetIndustryRank'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetMoneyRankSina(arg1) {
|
||||
return window['go']['main']['App']['GetMoneyRankSina'](arg1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function GetStockKLine(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['GetStockKLine'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function GetStockList(arg1) {
|
||||
return window['go']['main']['App']['GetStockList'](arg1);
|
||||
}
|
||||
|
||||
export function GetStockMinutePriceLineData(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetStockMinutePriceLineData'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetStockMoneyTrendByDay(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetStockMoneyTrendByDay'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetTelegraphList(arg1) {
|
||||
return window['go']['main']['App']['GetTelegraphList'](arg1);
|
||||
}
|
||||
|
||||
export function GetVersionInfo() {
|
||||
return window['go']['main']['App']['GetVersionInfo']();
|
||||
}
|
||||
@@ -62,12 +138,60 @@ export function GetfundList(arg1) {
|
||||
return window['go']['main']['App']['GetfundList'](arg1);
|
||||
}
|
||||
|
||||
export function GlobalStockIndexes() {
|
||||
return window['go']['main']['App']['GlobalStockIndexes']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function NewChatStream(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
|
||||
export function HotEvent(arg1) {
|
||||
return window['go']['main']['App']['HotEvent'](arg1);
|
||||
}
|
||||
|
||||
export function HotStock(arg1) {
|
||||
return window['go']['main']['App']['HotStock'](arg1);
|
||||
}
|
||||
|
||||
export function HotTopic(arg1) {
|
||||
return window['go']['main']['App']['HotTopic'](arg1);
|
||||
}
|
||||
|
||||
export function IndustryResearchReport(arg1) {
|
||||
return window['go']['main']['App']['IndustryResearchReport'](arg1);
|
||||
}
|
||||
|
||||
export function InvestCalendarTimeLine(arg1) {
|
||||
return window['go']['main']['App']['InvestCalendarTimeLine'](arg1);
|
||||
}
|
||||
|
||||
export function LongTigerRank(arg1) {
|
||||
return window['go']['main']['App']['LongTigerRank'](arg1);
|
||||
}
|
||||
|
||||
export function NewChatStream(arg1, arg2, arg3, arg4, arg5) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function RemoveGroup(arg1) {
|
||||
return window['go']['main']['App']['RemoveGroup'](arg1);
|
||||
}
|
||||
|
||||
export function RemoveStockGroup(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['RemoveStockGroup'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
|
||||
@@ -78,6 +202,18 @@ 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);
|
||||
}
|
||||
|
||||
export function SendDingDingMessage(arg1, arg2) {
|
||||
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
|
||||
}
|
||||
@@ -106,6 +242,18 @@ export function ShareAnalysis(arg1, arg2) {
|
||||
return window['go']['main']['App']['ShareAnalysis'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function StockNotice(arg1) {
|
||||
return window['go']['main']['App']['StockNotice'](arg1);
|
||||
}
|
||||
|
||||
export function StockResearchReport(arg1) {
|
||||
return window['go']['main']['App']['StockResearchReport'](arg1);
|
||||
}
|
||||
|
||||
export function SummaryStockNews(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function UnFollow(arg1) {
|
||||
return window['go']['main']['App']['UnFollow'](arg1);
|
||||
}
|
||||
|
||||
130
frontend/wailsjs/go/models.ts
Normal file → Executable file
130
frontend/wailsjs/go/models.ts
Normal file → Executable file
@@ -142,6 +142,94 @@ export namespace data {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class Group {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
name: string;
|
||||
sort: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Group(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.name = source["name"];
|
||||
this.sort = source["sort"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class GroupStock {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
stockCode: string;
|
||||
groupId: number;
|
||||
groupInfo: Group;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GroupStock(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.stockCode = source["stockCode"];
|
||||
this.groupId = source["groupId"];
|
||||
this.groupInfo = this.convertValues(source["groupInfo"], Group);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class FollowedStock {
|
||||
StockCode: string;
|
||||
Name: string;
|
||||
@@ -155,8 +243,9 @@ export namespace data {
|
||||
// Go type: time
|
||||
Time: any;
|
||||
Sort: number;
|
||||
Cron: string;
|
||||
Cron?: string;
|
||||
IsDel: number;
|
||||
Groups: GroupStock[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedStock(source);
|
||||
@@ -177,6 +266,7 @@ export namespace data {
|
||||
this.Sort = source["Sort"];
|
||||
this.Cron = source["Cron"];
|
||||
this.IsDel = source["IsDel"];
|
||||
this.Groups = this.convertValues(source["Groups"], GroupStock);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -198,6 +288,28 @@ export namespace data {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class SentimentResult {
|
||||
Score: number;
|
||||
Category: number;
|
||||
PositiveCount: number;
|
||||
NegativeCount: number;
|
||||
Description: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SentimentResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Score = source["Score"];
|
||||
this.Category = source["Category"];
|
||||
this.PositiveCount = source["PositiveCount"];
|
||||
this.NegativeCount = source["NegativeCount"];
|
||||
this.Description = source["Description"];
|
||||
}
|
||||
}
|
||||
export class Settings {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
@@ -229,6 +341,9 @@ export namespace data {
|
||||
enableNews: boolean;
|
||||
darkTheme: boolean;
|
||||
browserPoolSize: number;
|
||||
enableFund: boolean;
|
||||
enablePushNews: boolean;
|
||||
sponsorCode: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Settings(source);
|
||||
@@ -263,6 +378,9 @@ export namespace data {
|
||||
this.enableNews = source["enableNews"];
|
||||
this.darkTheme = source["darkTheme"];
|
||||
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 {
|
||||
@@ -308,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);
|
||||
@@ -336,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 {
|
||||
@@ -413,6 +535,7 @@ export namespace data {
|
||||
sort: number;
|
||||
alarmChangePercent: number;
|
||||
alarmPrice: number;
|
||||
Groups: GroupStock[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StockInfo(source);
|
||||
@@ -473,6 +596,7 @@ export namespace data {
|
||||
this.sort = source["sort"];
|
||||
this.alarmChangePercent = source["alarmChangePercent"];
|
||||
this.alarmPrice = source["alarmPrice"];
|
||||
this.Groups = this.convertValues(source["Groups"], GroupStock);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -582,7 +706,9 @@ export namespace models {
|
||||
icon: string;
|
||||
alipay: string;
|
||||
wxpay: string;
|
||||
wxgzh: string;
|
||||
buildTimeStamp: number;
|
||||
officialStatement: string;
|
||||
IsDel: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
@@ -600,7 +726,9 @@ 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"];
|
||||
}
|
||||
|
||||
|
||||
30
go.mod
30
go.mod
@@ -1,8 +1,6 @@
|
||||
module go-stock
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.0
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
@@ -10,15 +8,22 @@ require (
|
||||
github.com/coocood/freecache v1.2.4
|
||||
github.com/duke-git/lancet/v2 v2.3.4
|
||||
github.com/energye/systray v1.0.2
|
||||
github.com/gen2brain/beeep v0.11.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
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
|
||||
github.com/stretchr/testify v1.10.0
|
||||
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/sys v0.30.0
|
||||
golang.org/x/text v0.22.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
|
||||
gorm.io/gorm v1.25.12
|
||||
gorm.io/plugin/dbresolver v1.5.3
|
||||
@@ -26,6 +31,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
@@ -33,6 +39,7 @@ require (
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/esiqveland/notify v0.13.3 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
@@ -40,6 +47,7 @@ require (
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
@@ -53,23 +61,29 @@ require (
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
|
||||
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/vcaesar/cedar v0.20.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.33.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.35.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
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
|
||||
58
go.sum
58
go.sum
@@ -1,3 +1,5 @@
|
||||
git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=
|
||||
git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
|
||||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
@@ -14,6 +16,7 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
||||
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
|
||||
@@ -22,10 +25,16 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/energye/systray v1.0.2 h1:63R4prQkANtpM2CIA4UrDCuwZFt+FiygG77JYCsNmXc=
|
||||
github.com/energye/systray v1.0.2/go.mod h1:sp7Q/q/I4/w5ebvpSuJVep71s9Bg7L9ZVp69gBASehM=
|
||||
github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o=
|
||||
github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=
|
||||
github.com/gen2brain/beeep v0.11.1 h1:EbSIhrQZFDj1K2fzlMpAYlFOzV8YuNe721A58XcCTYI=
|
||||
github.com/gen2brain/beeep v0.11.1/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-ego/gse v0.80.3 h1:YNFkjMhlhQnUeuoFcUEd1ivh6SOB764rT8GDsEbDiEg=
|
||||
github.com/go-ego/gse v0.80.3/go.mod h1:Gt3A9Ry1Eso2Kza4MRaiZ7f2DTAvActmETY46Lxg0gU=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
||||
@@ -48,6 +57,10 @@ 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=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -89,6 +102,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
@@ -107,20 +122,44 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
|
||||
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
|
||||
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
|
||||
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw=
|
||||
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/vcaesar/cedar v0.20.2 h1:TDx7AdZhilKcfE1WvdToTJf5VrC/FXcUOW+KY1upLZ4=
|
||||
github.com/vcaesar/cedar v0.20.2/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik=
|
||||
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
||||
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
@@ -140,8 +179,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -159,8 +198,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -185,8 +224,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -206,8 +245,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -222,6 +261,9 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
|
||||
269
main.go
269
main.go
@@ -1,14 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"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"
|
||||
@@ -18,9 +18,8 @@ import (
|
||||
log "go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
@@ -38,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
|
||||
|
||||
@@ -51,68 +53,49 @@ var stocksBinUS []byte
|
||||
|
||||
var Version string
|
||||
var VersionCommit string
|
||||
var OFFICIAL_STATEMENT string
|
||||
var BuildKey string
|
||||
|
||||
func main() {
|
||||
checkDir("data")
|
||||
db.Init("")
|
||||
db.Dao.AutoMigrate(&data.StockInfo{})
|
||||
db.Dao.AutoMigrate(&data.StockBasic{})
|
||||
db.Dao.AutoMigrate(&data.FollowedStock{})
|
||||
db.Dao.AutoMigrate(&data.IndexBasic{})
|
||||
db.Dao.AutoMigrate(&data.Settings{})
|
||||
db.Dao.AutoMigrate(&models.AIResponseResult{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoHK{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoUS{})
|
||||
db.Dao.AutoMigrate(&data.FollowedFund{})
|
||||
db.Dao.AutoMigrate(&data.FundBasic{})
|
||||
db.Dao.AutoMigrate(&models.PromptTemplate{})
|
||||
go AutoMigrate()
|
||||
|
||||
if stocksBin != nil && len(stocksBin) > 0 {
|
||||
go initStockData()
|
||||
}
|
||||
log.SugaredLogger.Infof("init stocksBinHK %d", len(stocksBinHK))
|
||||
|
||||
if stocksBinHK != nil && len(stocksBinHK) > 0 {
|
||||
go initStockDataHK()
|
||||
}
|
||||
log.SugaredLogger.Infof("init stocksBinUS %d", len(stocksBinUS))
|
||||
|
||||
if stocksBinUS != nil && len(stocksBinUS) > 0 {
|
||||
go initStockDataUS()
|
||||
}
|
||||
updateBasicInfo()
|
||||
//db.Dao.Model(&data.Group{}).Where("id = ?", 0).FirstOrCreate(&data.Group{
|
||||
// Name: "默认分组",
|
||||
// Sort: 0,
|
||||
//})
|
||||
|
||||
// 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)
|
||||
@@ -123,12 +106,12 @@ func main() {
|
||||
//var width, height int
|
||||
//var err error
|
||||
//
|
||||
//width, height, err = getScreenResolution()
|
||||
//if err != nil {
|
||||
// log.SugaredLogger.Error("get screen resolution error")
|
||||
// width = 1456
|
||||
// height = 768
|
||||
//}
|
||||
width, _, minWidth, minHeight, err := getScreenResolution()
|
||||
if err != nil {
|
||||
log.SugaredLogger.Error("get screen resolution error")
|
||||
width = 1456
|
||||
//height = 768
|
||||
}
|
||||
|
||||
darkTheme := data.NewSettingsApi(&data.Settings{}).GetConfig().DarkTheme
|
||||
backgroundColour := &options.RGBA{R: 255, G: 255, B: 255, A: 1}
|
||||
@@ -136,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",
|
||||
//Width: width * 4 / 5,
|
||||
//Height: height * 4 / 5,
|
||||
MinWidth: 1456,
|
||||
MinHeight: 900,
|
||||
err = wails.Run(&options.App{
|
||||
Title: "go-stock:AI赋能股票分析✨",
|
||||
Width: width * 4 / 5,
|
||||
Height: 900,
|
||||
MinWidth: minWidth,
|
||||
MinHeight: minHeight,
|
||||
//MaxWidth: width,
|
||||
//MaxHeight: height,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: true,
|
||||
Frameless: false,
|
||||
StartHidden: false,
|
||||
HideWindowOnClose: false,
|
||||
EnableDefaultContextMenu: true,
|
||||
@@ -180,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,
|
||||
@@ -204,7 +188,30 @@ func main() {
|
||||
|
||||
}
|
||||
|
||||
func initStockDataUS() {
|
||||
func AutoMigrate() {
|
||||
db.Dao.AutoMigrate(&data.StockInfo{})
|
||||
db.Dao.AutoMigrate(&data.StockBasic{})
|
||||
db.Dao.AutoMigrate(&data.FollowedStock{})
|
||||
db.Dao.AutoMigrate(&data.IndexBasic{})
|
||||
db.Dao.AutoMigrate(&data.Settings{})
|
||||
db.Dao.AutoMigrate(&models.AIResponseResult{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoHK{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoUS{})
|
||||
db.Dao.AutoMigrate(&data.FollowedFund{})
|
||||
db.Dao.AutoMigrate(&data.FundBasic{})
|
||||
db.Dao.AutoMigrate(&models.PromptTemplate{})
|
||||
db.Dao.AutoMigrate(&data.Group{})
|
||||
db.Dao.AutoMigrate(&data.GroupStock{})
|
||||
db.Dao.AutoMigrate(&models.Tags{})
|
||||
db.Dao.AutoMigrate(&models.Telegraph{})
|
||||
db.Dao.AutoMigrate(&models.TelegraphTags{})
|
||||
db.Dao.AutoMigrate(&models.LongTigerRankData{})
|
||||
}
|
||||
|
||||
func initStockDataUS(ctx context.Context) {
|
||||
defer func() {
|
||||
go runtime.EventsEmit(ctx, "loadingMsg", "done")
|
||||
}()
|
||||
var v []models.StockInfoUS
|
||||
err := json.Unmarshal(stocksBinUS, &v)
|
||||
if err != nil {
|
||||
@@ -212,18 +219,25 @@ func initStockDataUS() {
|
||||
return
|
||||
}
|
||||
log.SugaredLogger.Infof("init stock data us %d", len(v))
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
//log.SugaredLogger.Infof("stock data us %s exist", item.Code)
|
||||
continue
|
||||
var total int64
|
||||
db.Dao.Model(&models.StockInfoUS{}).Count(&total)
|
||||
if total != int64(len(v)) {
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
//log.SugaredLogger.Infof("stock data us %s exist", item.Code)
|
||||
continue
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Create(&item)
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Create(&item)
|
||||
}
|
||||
}
|
||||
|
||||
func initStockDataHK() {
|
||||
func initStockDataHK(ctx context.Context) {
|
||||
defer func() {
|
||||
go runtime.EventsEmit(ctx, "loadingMsg", "done")
|
||||
}()
|
||||
var v []models.StockInfoHK
|
||||
err := json.Unmarshal(stocksBinHK, &v)
|
||||
if err != nil {
|
||||
@@ -231,15 +245,20 @@ func initStockDataHK() {
|
||||
return
|
||||
}
|
||||
log.SugaredLogger.Infof("init stock data hk %d", len(v))
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
//log.SugaredLogger.Infof("stock data hk %s exist", item.Code)
|
||||
continue
|
||||
var total int64
|
||||
db.Dao.Model(&models.StockInfoHK{}).Count(&total)
|
||||
if total != int64(len(v)) {
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
//log.SugaredLogger.Infof("stock data hk %s exist", item.Code)
|
||||
continue
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(&item)
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(&item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func updateBasicInfo() {
|
||||
@@ -251,7 +270,11 @@ func updateBasicInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
func initStockData() {
|
||||
func initStockData(ctx context.Context) {
|
||||
defer func() {
|
||||
go runtime.EventsEmit(ctx, "loadingMsg", "done")
|
||||
}()
|
||||
fields := "ts_code,symbol,name,area,industry,cnspell,market,list_date,act_name,act_ent_type,fullname,exchange,list_status,curr_type,enname,delist_date,is_hs"
|
||||
log.SugaredLogger.Info("init stock data")
|
||||
res := &data.TushareStockBasicResponse{}
|
||||
err := json.Unmarshal(stocksBin, res)
|
||||
@@ -259,26 +282,24 @@ func initStockData() {
|
||||
log.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range res.Data.Items {
|
||||
stock := &data.StockBasic{}
|
||||
stock.Exchange = convertor.ToString(item[0])
|
||||
stock.IsHs = convertor.ToString(item[1])
|
||||
stock.Name = convertor.ToString(item[2])
|
||||
stock.Industry = convertor.ToString(item[3])
|
||||
stock.ListStatus = convertor.ToString(item[4])
|
||||
stock.ActName = convertor.ToString(item[5])
|
||||
stock.ID = uint(item[6].(float64))
|
||||
stock.CurrType = convertor.ToString(item[7])
|
||||
stock.Area = convertor.ToString(item[8])
|
||||
stock.ListDate = convertor.ToString(item[9])
|
||||
stock.DelistDate = convertor.ToString(item[10])
|
||||
stock.ActEntType = convertor.ToString(item[11])
|
||||
stock.TsCode = convertor.ToString(item[12])
|
||||
stock.Symbol = convertor.ToString(item[13])
|
||||
stock.Cnspell = convertor.ToString(item[14])
|
||||
stock.Fullname = convertor.ToString(item[20])
|
||||
stock.Ename = convertor.ToString(item[21])
|
||||
|
||||
stockData := map[string]any{}
|
||||
for _, field := range strings.Split(fields, ",") {
|
||||
//logger.SugaredLogger.Infof("field: %s", field)
|
||||
idx := slice.IndexOf(res.Data.Fields, field)
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
stockData[field] = item[idx]
|
||||
}
|
||||
jsonData, _ := json.Marshal(stockData)
|
||||
err := json.Unmarshal(jsonData, stock)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
stock.ID = 0
|
||||
var count int64
|
||||
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Count(&count)
|
||||
if count > 0 {
|
||||
@@ -286,7 +307,38 @@ func initStockData() {
|
||||
} else {
|
||||
db.Dao.Create(stock)
|
||||
}
|
||||
|
||||
//db.Dao.Model(&data.StockBasic{}).FirstOrCreate(stock, &data.StockBasic{TsCode: stock.TsCode}).Where("ts_code = ?", stock.TsCode).Updates(stock)
|
||||
}
|
||||
|
||||
//for _, item := range res.Data.Items {
|
||||
// stock := &data.StockBasic{}
|
||||
// stock.Exchange = convertor.ToString(item[0])
|
||||
// stock.IsHs = convertor.ToString(item[1])
|
||||
// stock.Name = convertor.ToString(item[2])
|
||||
// stock.Industry = convertor.ToString(item[3])
|
||||
// stock.ListStatus = convertor.ToString(item[4])
|
||||
// stock.ActName = convertor.ToString(item[5])
|
||||
// stock.ID = uint(item[6].(float64))
|
||||
// stock.CurrType = convertor.ToString(item[7])
|
||||
// stock.Area = convertor.ToString(item[8])
|
||||
// stock.ListDate = convertor.ToString(item[9])
|
||||
// stock.DelistDate = convertor.ToString(item[10])
|
||||
// stock.ActEntType = convertor.ToString(item[11])
|
||||
// stock.TsCode = convertor.ToString(item[12])
|
||||
// stock.Symbol = convertor.ToString(item[13])
|
||||
// stock.Cnspell = convertor.ToString(item[14])
|
||||
// stock.Fullname = convertor.ToString(item[20])
|
||||
// stock.Ename = convertor.ToString(item[21])
|
||||
//
|
||||
// var count int64
|
||||
// db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Count(&count)
|
||||
// if count > 0 {
|
||||
// continue
|
||||
// } else {
|
||||
// db.Dao.Create(stock)
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
func checkDir(dir string) {
|
||||
@@ -295,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
23
utils.go
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user