Compare commits
406 Commits
v2025.2.9.
...
v2025.7.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
5de74f220f | ||
|
|
c5065b0504 | ||
|
|
9ebb246e5c | ||
|
|
5096bfac68 | ||
|
|
63e898bef8 | ||
|
|
7af3fe72d5 | ||
|
|
3402f0d296 | ||
|
|
51aae0539c | ||
|
|
7b625e2e80 | ||
|
|
f1e40e7d3b | ||
|
|
5f8556cc3d | ||
|
|
34e2de07fb | ||
|
|
b186a17a81 | ||
|
|
95c3909dc9 | ||
|
|
54b0c7ccb3 | ||
|
|
e44bc55301 | ||
|
|
fd3046b2c3 | ||
|
|
2b41dc11c1 | ||
|
|
076dc4f9ef | ||
|
|
1a728672c8 | ||
|
|
c8178a6c5f | ||
|
|
9d546fd214 | ||
|
|
d467adbdec | ||
|
|
c08776d028 | ||
|
|
c3c770b2ed | ||
|
|
63a05954f8 | ||
|
|
98c81107fc | ||
|
|
fb862564e1 | ||
|
|
c0bad34e36 | ||
|
|
f7a2681157 | ||
|
|
ee5c47f2dc | ||
|
|
c28151320c | ||
|
|
e5c4076278 | ||
|
|
8673796919 | ||
|
|
b4c513a585 | ||
|
|
f48aa837a9 | ||
|
|
e347f6080c | ||
|
|
b371555d37 | ||
|
|
4c3fa36d4f | ||
|
|
1d4ede336c | ||
|
|
740f8ef022 | ||
|
|
646420d672 | ||
|
|
55f7f246b0 | ||
|
|
0b4c9f9ae2 | ||
|
|
f6eaa11d65 | ||
|
|
11f0b66360 | ||
|
|
f0e5dbe278 | ||
|
|
e260e3fc71 | ||
|
|
c64f865216 | ||
|
|
3217338966 | ||
|
|
5d6ecdc21b | ||
|
|
6beaf9007c | ||
|
|
1925ffda31 | ||
|
|
217c4975c4 | ||
|
|
4dd474c0fb | ||
|
|
78150bcecd | ||
|
|
2e7c9514b4 | ||
|
|
ba862ff586 | ||
|
|
7d58082525 | ||
|
|
4f96b0a784 | ||
|
|
db43da6577 | ||
|
|
9a6e210bae | ||
|
|
1b31ff04df | ||
|
|
ebdd0d701e | ||
|
|
102d6bbcdb | ||
|
|
74746fc2c2 | ||
|
|
6f6884c18a | ||
|
|
ec7534ff2c | ||
|
|
db270779e6 | ||
|
|
09ae4c542b | ||
|
|
5b1a9c6d4d | ||
|
|
1c0596587f | ||
|
|
8e7b7bd4e1 | ||
|
|
48c5f5cc4c | ||
|
|
7417caa778 | ||
|
|
0864806770 | ||
|
|
adcde5efcc | ||
|
|
ce91b2e532 | ||
|
|
826a29cd8c | ||
|
|
b2b0300aa1 | ||
|
|
dbc25ca582 | ||
|
|
40a4e58276 | ||
|
|
2c2d689f53 | ||
|
|
fdca30ce3a | ||
|
|
7b3bad4102 | ||
|
|
531b01bca3 | ||
|
|
645c6979a4 | ||
|
|
5c94b40e4d | ||
|
|
83603a12a7 | ||
|
|
2aba86e424 | ||
|
|
8a7e0140eb | ||
|
|
797a35eaa5 | ||
|
|
1763435aa1 | ||
|
|
7952c1fceb | ||
|
|
fbb8b00315 | ||
|
|
2bf7d1e31f | ||
|
|
cb2bc61c6f | ||
|
|
b3f23fc4db | ||
|
|
67bd9e7996 | ||
|
|
4b9ae00452 | ||
|
|
4baaefc8c5 | ||
|
|
a6f17c632e | ||
|
|
4c249f0806 | ||
|
|
825014e370 | ||
|
|
c91466a023 | ||
|
|
92c61e4c26 | ||
|
|
b34d2d8d76 | ||
|
|
c287a82211 | ||
|
|
1144ac34a7 | ||
|
|
1b66f0c0d8 | ||
|
|
e597d3b484 | ||
|
|
cdc4b43925 | ||
|
|
0ff14fc01c | ||
|
|
5ccbbb6bb5 | ||
|
|
ec4a8659eb | ||
|
|
34ac6755a9 | ||
|
|
e21ba1b800 | ||
|
|
17a234f679 | ||
|
|
d504dc6d13 | ||
|
|
0b749d1699 | ||
|
|
4e9a24c8f2 | ||
|
|
c81b1a730d | ||
|
|
8d3cd7b151 | ||
|
|
5ee1ae4a32 | ||
|
|
dab51f7a70 | ||
|
|
a20d4e721d | ||
|
|
f4da21d645 | ||
|
|
fc37440f6b | ||
|
|
d7b47a7010 | ||
|
|
85d71ae58e | ||
|
|
23dc25f642 | ||
|
|
467bbd8923 | ||
|
|
6be5c0fa05 | ||
|
|
4fac915778 | ||
|
|
d27bcbd334 | ||
|
|
d46872ffbd | ||
|
|
29da37739d | ||
|
|
d7584bc4de | ||
|
|
1f78cc3589 | ||
|
|
a3b718c149 | ||
|
|
37e63538e2 | ||
|
|
70ee9df22a | ||
|
|
b764c978f1 | ||
|
|
fc8dbb919c | ||
|
|
02e3d1df11 | ||
|
|
e074ab2c39 | ||
|
|
e2e5a063e7 | ||
|
|
c4c2bea73d | ||
|
|
bfd9515387 | ||
|
|
7029d75790 | ||
|
|
13d1c75b76 | ||
|
|
aaf53f651a | ||
|
|
dad9ece712 | ||
|
|
6e17e89961 | ||
|
|
fae5a5fb6a | ||
|
|
96f2898111 | ||
|
|
e24965393b | ||
|
|
7e5d135483 | ||
|
|
c8827da35a | ||
|
|
268bc8b1f6 | ||
|
|
2869b37053 | ||
|
|
b237341fda | ||
|
|
957de8ad8b | ||
|
|
e622b7d86e | ||
|
|
95f9f1840f | ||
|
|
267f6f638f | ||
|
|
b459abb35d | ||
|
|
d79bdc8bc1 | ||
|
|
863e88c579 | ||
|
|
853f6b180e | ||
|
|
b4c55ce233 | ||
|
|
22111411c3 | ||
|
|
ddfc7c1216 | ||
|
|
908086c0c0 | ||
|
|
2c0e2ec698 | ||
|
|
cb4690bf88 | ||
|
|
100fb3e2a9 | ||
|
|
1bb13866bc | ||
|
|
05aaf82849 | ||
|
|
255a214554 | ||
|
|
5d0de949a8 | ||
|
|
03229fa46b | ||
|
|
b4cdd2d28b | ||
|
|
ef838af18b | ||
|
|
62defa1ebc | ||
|
|
3dd9790015 | ||
|
|
c25304d28b | ||
|
|
222ba03841 | ||
|
|
3f73a5a521 | ||
|
|
3a3e0b0543 | ||
|
|
0006501cc8 | ||
|
|
c5fbe5fdae | ||
|
|
66d85cf0a2 | ||
|
|
24145894b6 | ||
|
|
75f680a298 | ||
|
|
2658f207dc | ||
|
|
626f99f0d1 |
54
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Bug 报告
|
||||
|
||||
## 基本信息
|
||||
### Bug 描述
|
||||
<请用简洁明了的语言概括 Bug 的核心问题,例如:“登录页面输入错误密码后无提示信息”>
|
||||
|
||||
### 软件版本信息
|
||||
<说明你所使用的软件版本,在关于界面中可以找到>
|
||||
|
||||
### 运行操作系统和环境
|
||||
- **操作系统**:<例如 Windows 10、macOS 12.6、Ubuntu 22.04 等>
|
||||
- **浏览器(如果是网页应用)**:<如 Chrome 108、Firefox 107 等,同时说明浏览器的版本和是否使用了特殊的插件>
|
||||
- **其他相关环境信息**:<例如运行项目的服务器配置、数据库版本等>
|
||||
|
||||
## Bug 描述
|
||||
### 预期行为
|
||||
<详细描述你认为在正常情况下系统应该呈现的行为。例如:“当用户在登录页面输入错误密码时,页面应弹出提示框显示‘密码错误,请重新输入’”>
|
||||
|
||||
### 实际行为
|
||||
<准确描述实际发生的情况。可以包括错误信息、页面显示异常、功能无法正常使用等具体表现。例如:“当输入错误密码后,页面没有任何提示,也没有重新聚焦到密码输入框,登录按钮依然可点击”>
|
||||
|
||||
### 复现步骤
|
||||
<提供详细的步骤,让开发者能够按照这些步骤重现 Bug。步骤要尽量清晰、具体,例如:
|
||||
1. 打开项目的登录页面(URL:[具体登录页面 URL])。
|
||||
2. 在用户名输入框输入已注册的用户名。
|
||||
3. 在密码输入框输入错误的密码。
|
||||
4. 点击登录按钮。>
|
||||
|
||||
### 频率
|
||||
<说明 Bug 出现的频率,例如“每次都会出现”“偶尔出现(约 10% 的概率)”等>
|
||||
|
||||
## 相关信息
|
||||
### 错误日志
|
||||
<如果有错误日志或控制台输出信息,请提供完整的内容。可以使用代码块来展示,例如:>
|
||||
|
||||
|
||||
|
||||
### 截图或视频
|
||||
<如果 Bug 涉及页面显示问题或操作流程异常,附上相关的截图或录屏视频会非常有帮助。可以直接上传截图文件,或者提供视频的链接>
|
||||
|
||||
### 可能的原因分析(可选)
|
||||
<如果你对 Bug 产生的原因有一些初步的猜测或分析,可以在这里简要说明。这有助于开发者更快地定位问题,但不是必需的>
|
||||
|
||||
## 补充说明
|
||||
<如果有其他与 Bug 相关但不属于上述分类的信息,可以在这里进行补充,例如之前是否进行过特定的配置更改、是否与其他功能存在关联等>
|
||||
10
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
48
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# Pull Request 信息
|
||||
|
||||
## 本次 PR 概述
|
||||
请简要描述这个 Pull Request 做了什么改动。例如:
|
||||
- 修复了某个特定功能的 bug
|
||||
- 实现了一个新的功能特性
|
||||
- 对代码进行了优化,提升了性能
|
||||
|
||||
## 相关问题
|
||||
如果这个 PR 是为了解决某个 Issue,请在此处关联对应的 Issue 编号,格式为 `Fixes #<issue-number>`。例如:
|
||||
Fixes #123
|
||||
|
||||
## 改动内容详细说明
|
||||
### 代码修改
|
||||
- 列出主要修改的文件和修改点。例如:
|
||||
- `app_linux.go`:
|
||||
- 修改了函数 `GetStockList` 的逻辑,从使用 `for` 循环改为 `sum` 函数,提升了计算效率。
|
||||
- `app_test.go`:
|
||||
- 新增了针对 `GetStockList` 函数的单元测试,确保修改后的逻辑正确。
|
||||
|
||||
### 新增功能
|
||||
如果有新增功能,请详细描述该功能的使用方法和特点。例如:
|
||||
- 新增了一个用户认证模块,支持使用用户名和密码进行登录。使用方法如下:
|
||||
- 调用 `authenticate_user(username, password)` 函数进行认证。
|
||||
- 若认证成功,返回 `True`;否则返回 `False`。
|
||||
|
||||
### 删除内容
|
||||
如果有删除的代码或文件,请说明删除的原因。例如:
|
||||
- 删除了 `app_test.go` 文件,因为该模块的功能已经被新的模块替代,不再需要。
|
||||
|
||||
## 测试情况
|
||||
### 单元测试
|
||||
- 列出运行的单元测试以及测试结果。例如:
|
||||
- 运行了 `app_test.go` 进行单元测试,所有测试用例均通过。
|
||||
- 测试覆盖率达到了 90%。
|
||||
|
||||
### 集成测试
|
||||
如果进行了集成测试,请描述测试环境和测试结果。例如:
|
||||
- 在本地开发环境(Wails CLI v2.10.1 node v18.19.1 )中进行了集成测试,功能正常。
|
||||
- 在 CI/CD 环境中也进行了测试,所有步骤均通过。
|
||||
|
||||
## 注意事项
|
||||
- 提醒其他开发者在审查代码时需要注意的地方。例如:
|
||||
- 本次修改涉及到数据库表结构的变更,请确保在部署前进行数据库迁移。
|
||||
- 新增的功能依赖于第三方库 `requests`,请确保在环境中安装该库。
|
||||
|
||||
## 其他补充说明
|
||||
- 可以在这里提供任何其他需要说明的信息,例如设计文档的链接、相关讨论的记录等。
|
||||
17
.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:
|
||||
@@ -37,13 +43,16 @@ jobs:
|
||||
$commit_message = & git log -1 --pretty=format:"%s"
|
||||
echo "::set-output name=commit_message::$commit_message"
|
||||
|
||||
- name: Build wails
|
||||
uses: ArvinLovegood/wails-build-action@v2.8
|
||||
- name: Build wails x go-stock
|
||||
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 }}
|
||||
build-statement: ${{ env.OFFICIAL_STATEMENT }}
|
||||
build-key: ${{ env.BUILD_KEY }}
|
||||
node-version: '20.x'
|
||||
|
||||
3
.gitignore
vendored
@@ -109,4 +109,5 @@ dist
|
||||
/data/*.db
|
||||
/build/*.exe
|
||||
/build/bin/*
|
||||
frontend/package.json.md5
|
||||
frontend/package.json.md5
|
||||
/build/us.json
|
||||
|
||||
217
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,217 @@
|
||||
|
||||
# Contributor Covenant 行为准则
|
||||
|
||||
## 我们的承诺
|
||||
|
||||
我们作为项目的成员、贡献者和领导者,承诺为每一个人营造一个无骚扰的社区参与环境,无论年龄、体型、可见或不可见的残疾状况、种族、身体特征、性别认同与表达、经验水平、教育背景、社会经济地位、国籍、个人外貌、种族、宗教信仰或性取向如何。
|
||||
|
||||
我们承诺以有助于建立一个开放、友好、多元、包容和健康的社区的方式行事和互动。
|
||||
|
||||
## 我们的准则
|
||||
|
||||
有助于为我们的社区营造积极环境的行为示例包括:
|
||||
|
||||
- 对他人展现出同理心和善意
|
||||
- 尊重不同的意见、观点和经验
|
||||
- 给予并欣然接受建设性的反馈
|
||||
- 对自己的错误负责,向受影响的人道歉,并从经验中学习
|
||||
- 不仅关注个人利益,更着眼于整个社区的利益
|
||||
|
||||
不可接受的行为示例包括:
|
||||
|
||||
- 使用性暗示的语言或图像,以及任何形式的性关注或挑逗
|
||||
- 恶意挑衅、侮辱性或贬低性的评论,以及个人或政治攻击
|
||||
- 公开或私下的骚扰行为
|
||||
- 在未经明确许可的情况下公布他人的私人信息,如实际地址或电子邮件地址
|
||||
- 在专业环境中被合理认为不适当的其他行为
|
||||
|
||||
## 执行责任
|
||||
|
||||
社区领导者有责任阐明和执行我们可接受行为的标准,并将针对任何他们认为不适当、具有威胁性、冒犯性或有害的行为采取适当和公平的纠正措施。
|
||||
|
||||
社区领导者有权且有责任移除、编辑或拒绝不符合本行为准则的评论、提交的代码、代码修改、维基编辑、问题报告和其他贡献,并在适当时说明进行管理决策的原因。
|
||||
|
||||
## 适用范围
|
||||
|
||||
本行为准则适用于所有社区空间,并且当个人在公共场合正式代表社区时也同样适用。代表我们社区的示例包括使用官方电子邮件地址、通过官方社交媒体账户发布内容,或在线上或线下活动中担任指定代表。
|
||||
|
||||
## 执行
|
||||
|
||||
若发生滥用、骚扰或其他不可接受的行为,可向负责执行的社区领导者报告,邮箱地址为 [sparkmemory@163.com]。所有投诉都将得到及时、公正的审查和调查。
|
||||
|
||||
所有社区领导者都有义务尊重任何事件报告者的隐私和安全。
|
||||
|
||||
### 执行指南
|
||||
|
||||
社区领导者将遵循以下社区影响指南来确定对任何他们认为违反本行为准则的行为的后果:
|
||||
|
||||
#### 1. 纠正
|
||||
|
||||
**社区影响**:使用不适当的语言或其他被认为在社区中不专业或不受欢迎的行为。
|
||||
|
||||
**后果**:社区领导者发出私下的书面警告,阐明违规行为的性质,并解释为什么该行为不适当。可能会要求公开道歉。
|
||||
|
||||
#### 2. 警告
|
||||
|
||||
**社区影响**:通过单次事件或一系列行为构成的违规。
|
||||
|
||||
**后果**:发出警告并说明持续此类行为的后果。在指定的时间段内,禁止与相关人员进行互动,包括主动与执行本行为准则的人员进行互动。这包括避免在社区空间以及社交媒体等外部渠道进行互动。违反这些规定可能会导致临时或永久禁令。
|
||||
|
||||
#### 3. 临时禁令
|
||||
|
||||
**社区影响**:严重违反社区标准,包括持续的不当行为。
|
||||
|
||||
**后果**:在指定的时间段内,禁止与社区进行任何形式的互动或公开交流。在此期间,禁止与相关人员进行任何公开或私下的互动,包括主动与执行本行为准则的人员进行互动。违反这些规定可能会导致永久禁令。
|
||||
|
||||
#### 4. 永久禁令
|
||||
|
||||
**社区影响**:表现出违反社区标准的行为模式,包括持续的不当行为、骚扰个人,或对某类人群进行攻击或贬低。
|
||||
|
||||
**后果**:永久禁止在社区内进行任何形式的公开互动。
|
||||
|
||||
## 版权声明
|
||||
|
||||
本行为准则改编自 [Contributor Covenant][主页] 2.1 版本,可在 [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1] 查看。
|
||||
|
||||
社区影响指南的灵感来自 [Mozilla 的行为准则执行阶梯][Mozilla CoC]。
|
||||
|
||||
有关本行为准则常见问题的解答,请参阅常见问题解答页面 [https://www.contributor-covenant.org/faq][FAQ]。该准则有多种语言的翻译版本,可在 [https://www.contributor-covenant.org/translations][翻译] 查看。
|
||||
|
||||
[主页]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[翻译]: https://www.contributor-covenant.org/translations
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at [sparkmemory@163.com].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
### Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
#### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
#### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
#### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
#### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
|
||||
at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
79
CONTRIBUTING.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Contributing to [go-stock]
|
||||
|
||||
感谢你对 [go-stock] 项目的兴趣并愿意贡献代码!本指南将帮助你了解如何为这个项目做出贡献。
|
||||
|
||||
## 行为准则
|
||||
|
||||
在参与这个项目时,请遵守我们的 [行为准则](./CODE_OF_CONDUCT.md)。我们致力于为所有贡献者提供一个友好、包容和尊重的环境。
|
||||
|
||||
## 贡献类型
|
||||
|
||||
### 报告问题
|
||||
如果你发现了一个 bug、有功能请求或者对项目有任何建议,请在项目的 [GitHub Issues](https://github.com/ArvinLovegood/go-stock/issues) 中创建一个新的 issue。在创建 issue 时,请提供尽可能多的信息,包括:
|
||||
- **问题描述**:清晰地描述你遇到的问题或建议的功能。
|
||||
- **重现步骤**:如果是 bug,请提供重现该问题的具体步骤。
|
||||
- **环境信息**:例如操作系统、编程语言版本等。
|
||||
- **相关日志或错误信息**:如果有的话,请附上相关的日志或错误信息。
|
||||
|
||||
### 提交代码
|
||||
我们欢迎各种类型的代码贡献,包括修复 bug、添加新功能、改进文档等。请按照以下步骤提交你的代码:
|
||||
|
||||
#### 1. Fork 项目
|
||||
在 GitHub 上点击项目页面的 “Fork” 按钮,将项目复制到你自己的 GitHub 账户下。
|
||||
|
||||
#### 2. 克隆项目到本地
|
||||
使用以下命令将你 fork 的项目克隆到本地:
|
||||
```bash
|
||||
git clone https://github.com/ArvinLovegood/go-stock.git
|
||||
cd go-stock
|
||||
```
|
||||
|
||||
#### 3. 创建新分支
|
||||
在开始编写代码之前,创建一个新的分支来包含你的更改。建议使用一个描述性的分支名称,例如 `fix-bug-123` 或 `add-new-feature`。
|
||||
```bash
|
||||
git checkout -b 新分支名称
|
||||
```
|
||||
|
||||
#### 4. 编写代码
|
||||
在新分支上进行你的代码更改。请确保你的代码遵循项目的编码风格和规范。
|
||||
|
||||
#### 5. 测试代码
|
||||
在提交代码之前,请确保你的更改通过了项目的测试。如果项目没有测试,请考虑添加适当的测试。
|
||||
|
||||
#### 6. 提交更改
|
||||
将你的更改提交到本地仓库,并提供一个清晰、简洁的提交信息。
|
||||
```bash
|
||||
git add.
|
||||
git commit -m "描述你的更改,例如:修复了 #123 号 bug"
|
||||
```
|
||||
|
||||
#### 7. 同步上游仓库
|
||||
在推送代码之前,确保你的分支与上游仓库(原始项目)保持同步。
|
||||
```bash
|
||||
git remote add upstream https://github.com/ArvinLovegood/go-stock.git
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
```
|
||||
|
||||
#### 8. 推送更改
|
||||
将你的更改推送到你 fork 的 GitHub 仓库。
|
||||
```bash
|
||||
git push origin 新分支名称
|
||||
```
|
||||
|
||||
#### 9. 创建 Pull Request
|
||||
在 GitHub 上,导航到你 fork 的项目页面,点击 “New pull request” 按钮。选择你刚刚推送的分支,并提供一个清晰的描述,说明你的更改内容和目的。然后提交 pull request。
|
||||
|
||||
### 改进文档
|
||||
良好的文档对于项目的成功至关重要。如果你发现文档中有错误、不清楚的地方或者有可以改进的地方,请提交一个 issue 或者直接修改文档并提交 pull request。
|
||||
|
||||
## 代码风格和规范
|
||||
请遵循项目的代码风格和规范。如果项目中没有明确的规范,请参考以下通用准则:
|
||||
- **代码格式**:使用一致的缩进、空格和换行符。
|
||||
- **注释**:添加适当的注释来解释代码的功能和逻辑。
|
||||
- **命名规范**:使用有意义的变量名、函数名和类名。
|
||||
|
||||
## 许可证
|
||||
通过贡献代码,你同意你的贡献将根据项目的 [许可证](./LICENSE) 进行分发。
|
||||
|
||||
再次感谢你对项目的贡献!如果你有任何问题或需要帮助,请随时在 issue 中提问。
|
||||
2
LICENSE
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright [2025] [sparkmemory@163.com]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
183
README.md
@@ -1,25 +1,134 @@
|
||||

|
||||
## 自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具
|
||||
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
|
||||
- 欢迎大家提出宝贵的建议,欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧),谢谢。
|
||||
## BIG NEWS !!! 重大更新!!!
|
||||
- 2025.01.17 新增AI大模型分析股票功能
|
||||

|
||||
# go-stock : 基于大语言模型的AI赋能股票分析工具
|
||||
## 
|
||||

|
||||
[](https://github.com/ArvinLovegood/go-stock)
|
||||
[](https://gitee.com/arvinlovegood_admin/go-stock)
|
||||
|
||||
## 简介
|
||||
- 本项目基于Wails和NaiveUI,纯属无聊,仅供娱乐,不喜勿喷。
|
||||
[//]: # ([](https://gitcode.com/ArvinLovegood/go-stock))
|
||||
|
||||
### 🌟公众号
|
||||

|
||||
|
||||
### 📈 交流群
|
||||
- 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大模型构建的股票分析工具。
|
||||
- 目前已支持A股,港股,美股,未来计划加入基金,ETF等支持。
|
||||
- 支持市场整体/个股情绪分析,K线技术指标分析等功能。
|
||||
- 本项目仅供娱乐,不喜勿喷,AI分析股票结果仅供学习研究,投资有风险,请谨慎使用。
|
||||
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
|
||||
|
||||
### 📦 立即体验
|
||||
- 安装版:[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)
|
||||
|
||||
## Snapshot
|
||||
|
||||
### 💬 支持大模型/平台
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [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>💕
|
||||
- 优云智算(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自动定时分析功能
|
||||
### 2025.03.29 多提示词模板管理,AI分析时支持选择不同提示词模板
|
||||
### 2025.03.28 AI分析结果保存为markdown文件时,支持保存位置目录选择
|
||||
### 2025.03.15 自定义爬虫使用的浏览器路径配置
|
||||
### 2025.03.14 优化编译构建,大幅减少编译后的程序文件大小
|
||||
### 2025.03.09 基金估值和净值监控查看
|
||||
### 2025.03.06 项目社区分享功能
|
||||
### 2025.02.28 美股数据支持
|
||||
### 2025.02.23 弹幕功能,盯盘不再孤单,无聊划个水!😎
|
||||
### 2025.02.22 港股数据支持(目前有延迟)
|
||||
|
||||
### 2025.02.16 AI分析后可继续对话提问
|
||||
- [v2025.2.16.1-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.16.1-alpha)
|
||||
|
||||
### 2025.02.12 可配置的提问模板
|
||||
- [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha)
|
||||
|
||||
|
||||
## 🦄 重大更新
|
||||
### BIG NEWS !!! 重大更新!!!
|
||||
- 2025.04.25 市场资讯支持AI分析和总结:让AI帮你读市场!
|
||||

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

|
||||

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

|
||||
## 📸 功能截图
|
||||

|
||||
### 设置
|
||||

|
||||

|
||||
### 成本设置
|
||||

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

|
||||

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

|
||||
### 钉钉报警通知
|
||||
@@ -28,34 +137,40 @@
|
||||

|
||||
### 版本信息提示
|
||||

|
||||
## About
|
||||
|
||||
### A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
|
||||
A股数据可视化工具,基于Wails和NaiveUI。
|
||||
## 💕 感谢以下项目
|
||||
- [NaiveUI](https://www.naiveui.com/)
|
||||
- [Wails](https://wails.io/)
|
||||
- [Vue](https://vuejs.org/)
|
||||
- [Vite](https://vitejs.dev/)
|
||||
- [Tushare](https://tushare.pro/register?reg=701944)
|
||||
|
||||
## Prerequisites
|
||||
INSTALL [GO](https://golang.org) AND [Wails](https://wails.io/)
|
||||
## 😘 赞助我
|
||||
### 都划到这了,如果我的项目对您有帮助,请赞助我吧!😊😊😊
|
||||
| 支付宝 | 微信 |
|
||||
|-----|-----|
|
||||
|  |  |
|
||||
|
||||
## Running the Application in Developer Mode
|
||||
The easiest way is to use the Wails CLI: `wails dev`
|
||||
|
||||
This should hot refresh when making changes the Frontend and rebuild when making changes in the Go.
|
||||
## ⭐ Star History
|
||||
[](https://star-history.com/#ArvinLovegood/go-stock&Date)
|
||||
## 🤖 状态
|
||||

|
||||
|
||||
## 🐳 关于技术支持申明
|
||||
- 本软件基于开源技术构建,使用Wails、NaiveUI、Vue、AI大模型等开源项目。 技术上如有问题,可以先向对应的开源社区请求帮助。
|
||||
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系微信(备注 技术支持):ArvinLovegood
|
||||
|
||||
<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">
|
||||
|
||||
|
||||
| 技术支持方式 | 赞助(元) |
|
||||
|:--------------------------------|:-----:|
|
||||
| 加 QQ:506808970,微信:ArvinLovegood | 100/次 |
|
||||
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |
|
||||
|
||||
## Building the Application for Production
|
||||
|
||||
You can build you Application with: `wails build`
|
||||
|
||||
|
||||
## License
|
||||
[Apache License 2.0](LICENSE)
|
||||
|
||||
## Credits
|
||||
[NaiveUI](https://www.naiveui.com/)
|
||||
[Wails](https://wails.io/)
|
||||
[Vue](https://vuejs.org/)
|
||||
[Vite](https://vitejs.dev/)
|
||||
|
||||
## 都划到这了,如果我的项目对您有帮助,请赞助我吧!😊😊😊
|
||||
| 支付宝 | 微信 |
|
||||
|-----|-----|
|
||||
|  |  |
|
||||
56
SECURITY.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 安全策略
|
||||
|
||||
## 1. 受支持的版本
|
||||
|
||||
以下是 [go-stock] 项目当前接受安全更新支持的版本:
|
||||
|
||||
| 版本号 | 是否支持 |
|
||||
| ------- | ------------------ |
|
||||
| [v年.月.日.版本号] | :white_check_mark: |
|
||||
| [v较旧的年.月.日.版本号] | :x: |
|
||||
|
||||
请注意,通常只有最新的主要或次要版本会积极维护安全更新,较旧版本可能不会收到安全补丁。
|
||||
|
||||
## 2. 报告安全漏洞
|
||||
|
||||
### 2.1 报告方式
|
||||
我们非常重视安全漏洞问题。如果您在我们的项目中发现了安全漏洞,请通过以下方式向我们报告:
|
||||
|
||||
- **私下披露**:请发送电子邮件至 [sparkmemory@163.com]。在邮件中,请包含以下详细信息:
|
||||
- 对漏洞的详细描述,包括如何复现该漏洞。
|
||||
- 受影响的项目版本。
|
||||
- 该漏洞可能造成的任何影响或风险。
|
||||
- 如果可能,请提供建议的修复或缓解策略。
|
||||
|
||||
### 2.2 响应时间线
|
||||
- **首次确认**:我们将在收到报告后的 [7] 个工作日内确认收到您的报告。
|
||||
- **调查与进度更新**:我们将对报告的漏洞进行全面调查,并在 [7] 个工作日内向您提供调查进度更新。
|
||||
- **补丁发布**:一旦修复方案开发完成,我们将尽快发布补丁。发布补丁的时间可能会因漏洞的复杂程度而有所不同。
|
||||
|
||||
### 2.3 保密承诺
|
||||
我们深知安全漏洞报告保密的重要性。我们将对所有报告内容严格保密,未经您的许可,不会披露您的身份或漏洞的具体细节,除非法律有相关要求。
|
||||
|
||||
## 3. 安全更新与沟通
|
||||
|
||||
### 3.1 补丁发布
|
||||
当发现并修复安全漏洞后,我们会为受支持的项目版本发布补丁。补丁将在项目的官方 GitHub 仓库上提供。
|
||||
|
||||
### 3.2 安全公告
|
||||
我们会在项目的 GitHub 安全公告页面发布安全公告。这些公告将详细说明漏洞情况、受影响的版本以及缓解或修复问题的步骤。
|
||||
|
||||
### 3.3 沟通渠道
|
||||
- **GitHub**:所有关于安全更新和公告的官方通知将发布在项目的 GitHub 仓库上。
|
||||
- **电子邮件**:如果您订阅了项目的安全通知,您将收到有关重要安全更新的电子邮件通知。
|
||||
|
||||
## 4. 第三方依赖
|
||||
我们会定期审查和更新项目中使用的第三方依赖,以确保其安全性。然而,第三方组件的安全性也依赖于其各自的维护者。如果您发现与第三方依赖相关的安全问题,请同时向相应的维护者报告并告知我们。
|
||||
|
||||
## 5. 安全最佳实践
|
||||
我们鼓励项目的所有贡献者和用户遵循以下安全最佳实践:
|
||||
- 及时更新开发和生产环境,安装最新的安全补丁。
|
||||
- 使用强大的身份验证和授权机制。
|
||||
- 避免在代码中硬编码凭证信息。
|
||||
- 定期审查代码,排查潜在的安全漏洞。
|
||||
|
||||
## 6. 联系信息
|
||||
如果您对 [go-stock] 项目的安全策略有任何疑问或担忧,请通过 [sparkmemory@163.com] 联系我们。
|
||||
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
@@ -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
|
||||
}
|
||||
|
||||
25
app_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/24 9:35
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
func TestIsHKTradingTime(t *testing.T) {
|
||||
f := IsHKTradingTime(time.Now())
|
||||
t.Log(f)
|
||||
}
|
||||
|
||||
func TestIsUSTradingTime(t *testing.T) {
|
||||
|
||||
date := time.Now()
|
||||
hour, minute, _ := date.Clock()
|
||||
logger.SugaredLogger.Infof("当前时间: %d:%d", hour, minute)
|
||||
|
||||
t.Log(IsUSTradingTime(time.Now()))
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if getConfig().LocalPushEnable == false {
|
||||
if GetConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if getConfig().LocalPushEnable == false {
|
||||
if GetConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
||||
237
backend/data/crawler_api.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/chromedp/chromedp"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/13 9:25
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
type CrawlerApi struct {
|
||||
crawlerCtx context.Context
|
||||
crawlerBaseInfo CrawlerBaseInfo
|
||||
pool *BrowserPool
|
||||
}
|
||||
|
||||
func (c *CrawlerApi) NewTimeOutCrawler(timeout int, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
return c.NewCrawler(ctx, crawlerBaseInfo)
|
||||
}
|
||||
func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
|
||||
return CrawlerApi{
|
||||
crawlerCtx: ctx,
|
||||
crawlerBaseInfo: crawlerBaseInfo,
|
||||
pool: NewBrowserPool(GetConfig().BrowserPoolSize),
|
||||
}
|
||||
}
|
||||
func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bool) {
|
||||
page, err := c.pool.FetchPage(url, waitVisible)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return page, true
|
||||
}
|
||||
func (c *CrawlerApi) GetHtml_old(url, waitVisible string, headless bool) (string, bool) {
|
||||
htmlContent := ""
|
||||
path := GetConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("Browser path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
c.crawlerCtx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", headless),
|
||||
chromedp.Flag("blink-settings", "imagesEnabled=false"),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
defer pcancel()
|
||||
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
//defer chromedp.Cancel(ctx)
|
||||
err := chromedp.Run(ctx, chromedp.Navigate(url),
|
||||
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
|
||||
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
|
||||
chromedp.InnerHTML("body", &htmlContent),
|
||||
)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false
|
||||
}
|
||||
} else {
|
||||
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
//defer chromedp.Cancel(ctx)
|
||||
err := chromedp.Run(ctx, chromedp.Navigate(url), chromedp.WaitVisible("body"), chromedp.InnerHTML("body", &htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return htmlContent, true
|
||||
|
||||
}
|
||||
|
||||
func (c *CrawlerApi) GetHtmlWithNoCancel(url, waitVisible string, headless bool) (html string, ok bool, parent context.CancelFunc, child context.CancelFunc) {
|
||||
htmlContent := ""
|
||||
path := GetConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("BrowserPath :%s", path)
|
||||
var parentCancel context.CancelFunc
|
||||
var childCancel context.CancelFunc
|
||||
var pctx context.Context
|
||||
var cctx context.Context
|
||||
|
||||
if path != "" {
|
||||
pctx, parentCancel = chromedp.NewExecAllocator(
|
||||
c.crawlerCtx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", headless),
|
||||
chromedp.Flag("blink-settings", "imagesEnabled=false"),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
//defer pcancel()
|
||||
cctx, childCancel = chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
//defer cancel()
|
||||
err := chromedp.Run(cctx, chromedp.Navigate(url),
|
||||
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
|
||||
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
|
||||
chromedp.InnerHTML("body", &htmlContent),
|
||||
)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false, parentCancel, childCancel
|
||||
}
|
||||
} else {
|
||||
cctx, childCancel = chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
//defer cancel()
|
||||
err := chromedp.Run(cctx, chromedp.Navigate(url), chromedp.WaitVisible("body"), chromedp.InnerHTML("body", &htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false, parentCancel, childCancel
|
||||
}
|
||||
}
|
||||
return htmlContent, true, parentCancel, childCancel
|
||||
|
||||
}
|
||||
|
||||
func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless bool) (string, bool) {
|
||||
htmlContent := ""
|
||||
*actions = append(*actions, chromedp.InnerHTML("body", &htmlContent))
|
||||
|
||||
path := GetConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
c.crawlerCtx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", headless),
|
||||
chromedp.Flag("blink-settings", "imagesEnabled=false"),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
defer pcancel()
|
||||
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
//defer chromedp.Cancel(ctx)
|
||||
|
||||
err := chromedp.Run(ctx, *actions...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false
|
||||
}
|
||||
} else {
|
||||
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
//defer chromedp.Cancel(ctx)
|
||||
|
||||
err := chromedp.Run(ctx, *actions...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
return htmlContent, true
|
||||
}
|
||||
|
||||
type CrawlerBaseInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
BaseUrl string `json:"base_url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
371
backend/data/crawler_api_test.go
Normal file
@@ -0,0 +1,371 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewTimeOutGuShiTongCrawler(t *testing.T) {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
timeout := 10
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://gushitong.baidu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
|
||||
result := crawlerAPI.NewTimeOutCrawler(timeout, crawlerBaseInfo)
|
||||
assert.NotNil(t, result.crawlerCtx)
|
||||
assert.Equal(t, crawlerBaseInfo, result.crawlerBaseInfo)
|
||||
}
|
||||
|
||||
func TestNewGuShiTongCrawler(t *testing.T) {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://gushitong.baidu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
|
||||
result := crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
assert.Equal(t, ctx, result.crawlerCtx)
|
||||
assert.Equal(t, crawlerBaseInfo, result.crawlerBaseInfo)
|
||||
}
|
||||
|
||||
func TestGetHtml(t *testing.T) {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://gushitong.baidu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
url := "https://www.cls.cn/searchPage?type=depth&keyword=%E6%96%B0%E5%B8%8C%E6%9C%9B"
|
||||
waitVisible := ".search-telegraph-list,.subject-interest-list"
|
||||
|
||||
//url = "https://gushitong.baidu.com/stock/ab-600745"
|
||||
//waitVisible = "div.news-item"
|
||||
htmlContent, success := crawlerAPI.GetHtml(url, waitVisible, true)
|
||||
if success {
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
var messages []string
|
||||
document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
messages = append(messages, text)
|
||||
logger.SugaredLogger.Infof("搜索到消息-%s: %s", "", text)
|
||||
})
|
||||
}
|
||||
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
|
||||
}
|
||||
|
||||
func TestGetHtmlWithActions(t *testing.T) {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, CrawlerBaseInfo{
|
||||
Name: "百度股市通",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://gushitong.baidu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
})
|
||||
actions := []chromedp.Action{
|
||||
chromedp.Navigate("https://gushitong.baidu.com/stock/ab-600745"),
|
||||
chromedp.WaitVisible("div.cos-tab"),
|
||||
chromedp.Click(".header div.cos-tab:nth-child(6)", chromedp.ByQuery),
|
||||
chromedp.ScrollIntoView("div.finance-container >div.row:nth-child(3)"),
|
||||
chromedp.WaitVisible("div.cos-tabs-header-container"),
|
||||
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(1)", chromedp.ByQuery),
|
||||
chromedp.WaitVisible(".page-content .finance-container .report-col-content", chromedp.ByQuery),
|
||||
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(4)", chromedp.ByQuery),
|
||||
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight);`, nil),
|
||||
chromedp.Sleep(1 * time.Second),
|
||||
}
|
||||
htmlContent, success := crawlerAPI.GetHtmlWithActions(&actions, false)
|
||||
if success {
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
var messages []string
|
||||
document.Find("div.report-table-list-container,div.report-row").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(selection.Text(), false)
|
||||
messages = append(messages, text)
|
||||
logger.SugaredLogger.Infof("搜索到消息-%s: %s", "", text)
|
||||
})
|
||||
logger.SugaredLogger.Infof("messages:%d", len(messages))
|
||||
}
|
||||
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
|
||||
}
|
||||
|
||||
func TestHk(t *testing.T) {
|
||||
//https://stock.finance.sina.com.cn/hkstock/quotes/00001.html
|
||||
db.Init("../../data/stock.db")
|
||||
hks := &[]models.StockInfoHK{}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Limit(1).Find(hks)
|
||||
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://stock.finance.sina.com.cn",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
for _, hk := range *hks {
|
||||
logger.SugaredLogger.Infof("hk: %+v", hk)
|
||||
url := fmt.Sprintf("https://stock.finance.sina.com.cn/hkstock/quotes/%s.html", strings.ReplaceAll(hk.Code, ".HK", ""))
|
||||
htmlContent, ok := crawlerAPI.GetHtml(url, "#stock_cname", true)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
//logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
document.Find("#stock_cname").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
logger.SugaredLogger.Infof("股票名称-:%s", text)
|
||||
})
|
||||
|
||||
document.Find("#mts_stock_hk_price").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
logger.SugaredLogger.Infof("股票名称-现价: %s", text)
|
||||
})
|
||||
|
||||
document.Find(".deta_hqContainer >.deta03 li").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
logger.SugaredLogger.Infof("股票名称-%s: %s", "", text)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUSName(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
us := &[]models.StockInfoUS{}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("name = ?", "").Order("RANDOM()").Find(us)
|
||||
|
||||
for _, us := range *us {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://stock.finance.sina.com.cn",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", us.Code[:len(us.Code)-3])
|
||||
logger.SugaredLogger.Infof("url: %s", url)
|
||||
//waitVisible := "span.quote_title_name"
|
||||
waitVisible := "div.hq_title > h1"
|
||||
|
||||
htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true)
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
//logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
name := ""
|
||||
document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
|
||||
name = strutil.RemoveNonPrintable(selection.Text())
|
||||
name = strutil.SplitAndTrim(name, " ", "")[0]
|
||||
logger.SugaredLogger.Infof("股票名称-:%s", name)
|
||||
})
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", us.Code).Updates(map[string]interface{}{
|
||||
"name": name,
|
||||
"full_name": name,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
func TestUS(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
bytes, err := os.ReadFile("../../build/us.json")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://quote.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
tick := &Tick{}
|
||||
json.Unmarshal(bytes, &tick)
|
||||
for i, datum := range tick.Data {
|
||||
logger.SugaredLogger.Infof("datum: %d, %+v", i, datum)
|
||||
name := ""
|
||||
|
||||
//https://quote.eastmoney.com/us/AAPL.html
|
||||
//https://stock.finance.sina.com.cn/usstock/quotes/goog.html
|
||||
//url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", strings.ReplaceAll(datum.C, ".US", ""))
|
||||
////waitVisible := "span.quote_title_name"
|
||||
//waitVisible := "div.hq_title > h1"
|
||||
//
|
||||
//htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true)
|
||||
//
|
||||
//if !ok {
|
||||
// continue
|
||||
//}
|
||||
////logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
|
||||
//document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Error(err.Error())
|
||||
//}
|
||||
//document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
|
||||
// name = strutil.RemoveNonPrintable(selection.Text())
|
||||
// name = strutil.SplitAndTrim(name, " ", "")[0]
|
||||
// logger.SugaredLogger.Infof("股票名称-:%s", name)
|
||||
//})
|
||||
|
||||
us := &models.StockInfoUS{
|
||||
Code: datum.C + ".US",
|
||||
EName: datum.N,
|
||||
FullName: datum.N,
|
||||
Name: name,
|
||||
Exchange: datum.E,
|
||||
Type: datum.T,
|
||||
}
|
||||
db.Dao.Create(us)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUSSINA(t *testing.T) {
|
||||
//https://finance.sina.com.cn/stock/usstock/sector.shtml#cm
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://quote.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
html, ok := crawlerAPI.GetHtml("https://finance.sina.com.cn/stock/usstock/sector.shtml#cm", "div#data", false)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
document.Find("div#data > table >tbody >tr").Each(func(i int, selection *goquery.Selection) {
|
||||
tr := selection.Text()
|
||||
logger.SugaredLogger.Infof("tr: %s", tr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSina(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
url := "https://finance.sina.com.cn/realstock/company/sz002906/nc.shtml"
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://finance.sina.com.cn",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
html, ok := crawlerAPI.GetHtml(url, "div#hqDetails table", true)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
|
||||
//price
|
||||
price := strutil.RemoveWhiteSpace(document.Find("div#price").First().Text(), false)
|
||||
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString("\n ## 当前股票数据:\n")
|
||||
markdown.WriteString(fmt.Sprintf("### 当前股价:%s 时间:%s\n", price, hqTime))
|
||||
GetTableMarkdown(document, "div#hqDetails table", &markdown)
|
||||
|
||||
}
|
||||
|
||||
func TestDC(t *testing.T) {
|
||||
url := "https://emweb.securities.eastmoney.com/pc_hsf10/pages/index.html?type=web&code=sh600745#/cwfx"
|
||||
db.Init("../../data/stock.db")
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://emweb.securities.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString("\n ## 财务数据:\n")
|
||||
html, ok := crawlerAPI.GetHtml(url, "div.report_table table", false)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
GetTableMarkdown(document, "div.report_table table", &markdown)
|
||||
|
||||
}
|
||||
|
||||
type Tick struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Data []struct {
|
||||
C string `json:"c"`
|
||||
N string `json:"n"`
|
||||
T string `json:"t"`
|
||||
E string `json:"e"`
|
||||
} `json:"data"`
|
||||
}
|
||||
@@ -21,8 +21,8 @@ func NewDingDingAPI() *DingDingAPI {
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
if getConfig().DingPushEnable == false {
|
||||
logger.SugaredLogger.Info("钉钉推送未开启")
|
||||
if GetConfig().DingPushEnable == false {
|
||||
//logger.SugaredLogger.Info("钉钉推送未开启")
|
||||
return "钉钉推送未开启"
|
||||
}
|
||||
// 发送钉钉消息
|
||||
@@ -37,11 +37,11 @@ func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
|
||||
return "发送钉钉消息成功"
|
||||
}
|
||||
func getConfig() *Settings {
|
||||
func GetConfig() *Settings {
|
||||
return NewSettingsApi(&Settings{}).GetConfig()
|
||||
}
|
||||
func getApiURL() string {
|
||||
return getConfig().DingRobot
|
||||
return GetConfig().DingRobot
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendToDingDing(title, message string) string {
|
||||
|
||||
401
backend/data/fund_data_api.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FundApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
}
|
||||
|
||||
func NewFundApi() *FundApi {
|
||||
return &FundApi{
|
||||
client: resty.New(),
|
||||
config: GetConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
type FollowedFund struct {
|
||||
gorm.Model
|
||||
Code string `json:"code" gorm:"index"` // 基金代码
|
||||
Name string `json:"name"` // 基金简称
|
||||
|
||||
NetUnitValue *float64 `json:"netUnitValue"` // 单位净值
|
||||
NetUnitValueDate string `json:"netUnitValueDate"` // 单位净值日期
|
||||
NetEstimatedUnit *float64 `json:"netEstimatedUnit"` // 估算单位净值
|
||||
NetEstimatedTime string `json:"netEstimatedUnitTime"` // 估算单位净值日期
|
||||
NetAccumulated *float64 `json:"netAccumulated"` // 累计净值
|
||||
|
||||
//计算值
|
||||
NetEstimatedRate *float64 `json:"netEstimatedRate"` // 估算单位净值涨跌幅
|
||||
|
||||
FundBasic FundBasic `json:"fundBasic" gorm:"foreignKey:Code;references:Code"`
|
||||
}
|
||||
|
||||
func (FollowedFund) TableName() string {
|
||||
return "followed_fund"
|
||||
}
|
||||
|
||||
// FundBasic 基金基本信息结构体
|
||||
type FundBasic struct {
|
||||
gorm.Model
|
||||
Code string `json:"code" gorm:"index"` // 基金代码
|
||||
Name string `json:"name"` // 基金简称
|
||||
FullName string `json:"fullName"` // 基金全称
|
||||
Type string `json:"type"` // 基金类型
|
||||
Establishment string `json:"establishment"` // 成立日期
|
||||
Scale string `json:"scale"` // 最新规模(亿元)
|
||||
Company string `json:"company"` // 基金管理人
|
||||
Manager string `json:"manager"` // 基金经理
|
||||
Rating string `json:"rating"` //基金评级
|
||||
TrackingTarget string `json:"trackingTarget"` //跟踪标的
|
||||
|
||||
NetUnitValue *float64 `json:"netUnitValue"` // 单位净值
|
||||
NetUnitValueDate string `json:"netUnitValueDate"` // 单位净值日期
|
||||
NetEstimatedUnit *float64 `json:"netEstimatedUnit"` // 估算单位净值
|
||||
NetEstimatedTime string `json:"netEstimatedUnitTime"` // 估算单位净值日期
|
||||
NetAccumulated *float64 `json:"netAccumulated"` // 累计净值
|
||||
|
||||
//净值涨跌幅: 近1月,近3月,近6月,近1年,近3年,近5年,今年来,成立来
|
||||
NetGrowth1 *float64 `json:"netGrowth1"` //近1月
|
||||
NetGrowth3 *float64 `json:"netGrowth3"` //近3月
|
||||
NetGrowth6 *float64 `json:"netGrowth6"` //近6月
|
||||
NetGrowth12 *float64 `json:"netGrowth12"` //近1年
|
||||
NetGrowth36 *float64 `json:"netGrowth36"` //近3年
|
||||
NetGrowth60 *float64 `json:"netGrowth60"` //近5年
|
||||
NetGrowthYTD *float64 `json:"netGrowthYTD"` //今年来
|
||||
NetGrowthAll *float64 `json:"netGrowthAll"` //成立来
|
||||
}
|
||||
|
||||
func (FundBasic) TableName() string {
|
||||
return "fund_basic"
|
||||
}
|
||||
|
||||
// CrawlFundBasic 爬取基金基本信息
|
||||
func (f *FundApi) CrawlFundBasic(fundCode string) (*FundBasic, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("CrawlFundBasic panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
crawler := CrawlerApi{
|
||||
crawlerBaseInfo: CrawlerBaseInfo{
|
||||
Name: "天天基金",
|
||||
BaseUrl: "http://fund.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(f.config.CrawlTimeOut)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
crawler = crawler.NewCrawler(ctx, crawler.crawlerBaseInfo)
|
||||
url := fmt.Sprintf("%s/%s.html", crawler.crawlerBaseInfo.BaseUrl, fundCode)
|
||||
//logger.SugaredLogger.Infof("CrawlFundBasic url:%s", url)
|
||||
|
||||
// 使用现有爬虫框架解析页面
|
||||
htmlContent, ok := crawler.GetHtml(url, ".merchandiseDetail", true)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("页面解析失败")
|
||||
}
|
||||
|
||||
fund := &FundBasic{Code: fundCode}
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析基础信息
|
||||
name := doc.Find(".merchandiseDetail .fundDetail-tit").First().Text()
|
||||
fund.Name = strings.TrimSpace(strutil.ReplaceWithMap(name, map[string]string{"查看相关ETF>": ""}))
|
||||
//logger.SugaredLogger.Infof("基金名称:%s", fund.Name)
|
||||
|
||||
doc.Find(".infoOfFund table td ").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(s.Text(), true)
|
||||
//logger.SugaredLogger.Infof("基金信息:%+v", text)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
//logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
splitEx := strutil.SplitEx(text, ":", true)
|
||||
if strutil.ContainsAny(text, []string{"基金类型", "类型"}) {
|
||||
fund.Type = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"成立日期", "成立日"}) {
|
||||
fund.Establishment = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"基金规模", "规模"}) {
|
||||
fund.Scale = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"管理人", "基金公司"}) {
|
||||
fund.Company = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"基金经理", "经理人"}) {
|
||||
fund.Manager = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"基金评级", "评级"}) {
|
||||
fund.Rating = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"跟踪标的", "标的"}) {
|
||||
fund.TrackingTarget = splitEx[1]
|
||||
}
|
||||
})
|
||||
|
||||
//获取基金净值涨跌幅信息
|
||||
doc.Find(".dataOfFund dl > dd").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(s.Text(), true)
|
||||
//logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", text)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
//logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
splitEx := strutil.SplitAndTrim(text, ":", "%")
|
||||
toFloat, err1 := convertor.ToFloat(splitEx[1])
|
||||
if err1 != nil {
|
||||
//logger.SugaredLogger.Errorf("转换失败:%+v", err)
|
||||
return
|
||||
}
|
||||
//logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", toFloat)
|
||||
if strutil.ContainsAny(text, []string{"近1月"}) {
|
||||
fund.NetGrowth1 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近3月"}) {
|
||||
fund.NetGrowth3 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近6月"}) {
|
||||
fund.NetGrowth6 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近1年"}) {
|
||||
fund.NetGrowth12 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近3年"}) {
|
||||
fund.NetGrowth36 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近5年"}) {
|
||||
fund.NetGrowth60 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"今年来"}) {
|
||||
fund.NetGrowthYTD = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"成立来"}) {
|
||||
fund.NetGrowthAll = &toFloat
|
||||
}
|
||||
})
|
||||
//doc.Find(".dataOfFund dl > dd.dataNums,.dataOfFund dl > dt").Each(func(i int, s *goquery.Selection) {
|
||||
// //text := s.Text()
|
||||
// defer func() {
|
||||
// if r := recover(); r != nil {
|
||||
// //logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
// }
|
||||
// }()
|
||||
// //logger.SugaredLogger.Infof("净值信息:%+v", text)
|
||||
//})
|
||||
|
||||
//logger.SugaredLogger.Infof("基金信息:%+v", fund)
|
||||
|
||||
count := int64(0)
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Count(&count)
|
||||
if count == 0 {
|
||||
db.Dao.Create(fund)
|
||||
} else {
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
|
||||
return fund, nil
|
||||
}
|
||||
|
||||
func (f *FundApi) GetFundList(key string) []FundBasic {
|
||||
var funds []FundBasic
|
||||
db.Dao.Where("code like ? or name like ?", "%"+key+"%", "%"+key+"%").Limit(10).Find(&funds)
|
||||
return funds
|
||||
}
|
||||
|
||||
func (f *FundApi) GetFollowedFund() []FollowedFund {
|
||||
var funds []FollowedFund
|
||||
db.Dao.Preload("FundBasic").Find(&funds)
|
||||
for i, fund := range funds {
|
||||
if fund.NetUnitValue != nil && fund.NetEstimatedUnit != nil && *fund.NetUnitValue > 0 {
|
||||
netEstimatedRate := (*(funds[i].NetEstimatedUnit) - *(funds[i].NetUnitValue)) / *(fund.NetUnitValue) * 100
|
||||
netEstimatedRate = mathutil.RoundToFloat(netEstimatedRate, 2)
|
||||
funds[i].NetEstimatedRate = &netEstimatedRate
|
||||
}
|
||||
|
||||
}
|
||||
return funds
|
||||
}
|
||||
func (f *FundApi) FollowFund(fundCode string) string {
|
||||
var fund FundBasic
|
||||
db.Dao.Where("code=?", fundCode).First(&fund)
|
||||
if fund.Code != "" {
|
||||
follow := &FollowedFund{
|
||||
Code: fundCode,
|
||||
Name: fund.Name,
|
||||
}
|
||||
err := db.Dao.Model(follow).Where("code = ?", fundCode).FirstOrCreate(follow, "code", fund.Code).Error
|
||||
if err != nil {
|
||||
return "关注失败"
|
||||
}
|
||||
return "关注成功"
|
||||
} else {
|
||||
return "基金信息不存在"
|
||||
}
|
||||
}
|
||||
func (f *FundApi) UnFollowFund(fundCode string) string {
|
||||
var fund FollowedFund
|
||||
db.Dao.Where("code=?", fundCode).First(&fund)
|
||||
if fund.Code != "" {
|
||||
err := db.Dao.Model(&fund).Delete(&fund).Error
|
||||
if err != nil {
|
||||
return "取消关注失败"
|
||||
}
|
||||
return "取消关注成功"
|
||||
} else {
|
||||
return "基金信息不存在"
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FundApi) AllFund() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
//logger.SugaredLogger.Errorf("AllFund panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36").
|
||||
Get("https://fund.eastmoney.com/allfund.html")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//中文编码
|
||||
htmlContent := GB18030ToUTF8(response.Body())
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
cnt := 0
|
||||
doc.Find("ul.num_right li").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.SplitEx(s.Text(), "|", true)
|
||||
if len(text) > 0 {
|
||||
cnt++
|
||||
name := text[0]
|
||||
str := strutil.SplitAndTrim(name, ")", "(", ")")
|
||||
//logger.SugaredLogger.Infof("%d,基金信息 code:%s,name:%s", cnt, str[0], str[1])
|
||||
//go f.CrawlFundBasic(str[0])
|
||||
fund := &FundBasic{
|
||||
Code: str[0],
|
||||
Name: str[1],
|
||||
}
|
||||
count := int64(0)
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Count(&count)
|
||||
if count == 0 {
|
||||
db.Dao.Create(fund)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type FundNetUnitValue struct {
|
||||
Fundcode string `json:"fundcode"`
|
||||
Name string `json:"name"`
|
||||
Jzrq string `json:"jzrq"`
|
||||
Dwjz string `json:"dwjz"`
|
||||
Gsz string `json:"gsz"`
|
||||
Gszzl string `json:"gszzl"`
|
||||
Gztime string `json:"gztime"`
|
||||
}
|
||||
|
||||
// CrawlFundNetEstimatedUnit 爬取净值估算值
|
||||
func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
|
||||
var fundNetUnitValue FundNetUnitValue
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36").
|
||||
SetHeader("Referer", "https://fund.eastmoney.com/").
|
||||
SetQueryParams(map[string]string{"rt": strconv.FormatInt(time.Now().UnixMilli(), 10)}).
|
||||
Get(fmt.Sprintf("https://fundgz.1234567.com.cn/js/%s.js", code))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
if response.StatusCode() == 200 {
|
||||
htmlContent := string(response.Body())
|
||||
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
|
||||
if strings.Contains(htmlContent, "jsonpgz") {
|
||||
htmlContent = strutil.Trim(htmlContent, "jsonpgz(", ");")
|
||||
htmlContent = strutil.Trim(htmlContent, ");")
|
||||
//logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent)
|
||||
err := json.Unmarshal([]byte(htmlContent), &fundNetUnitValue)
|
||||
if err != nil {
|
||||
//logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
fund := &FollowedFund{
|
||||
Code: fundNetUnitValue.Fundcode,
|
||||
Name: fundNetUnitValue.Name,
|
||||
NetEstimatedTime: fundNetUnitValue.Gztime,
|
||||
}
|
||||
netEstimatedUnit, err := convertor.ToFloat(fundNetUnitValue.Gsz)
|
||||
if err == nil {
|
||||
fund.NetEstimatedUnit = &netEstimatedUnit
|
||||
}
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CrawlFundNetUnitValue 爬取净值
|
||||
func (f *FundApi) CrawlFundNetUnitValue(code string) {
|
||||
// var fundNetUnitValue FundNetUnitValue
|
||||
url := fmt.Sprintf("http://hq.sinajs.cn/rn=%d&list=f_%s", time.Now().UnixMilli(), code)
|
||||
//logger.SugaredLogger.Infof("url:%s", url)
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "hq.sinajs.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").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
if response.StatusCode() == 200 {
|
||||
data := string(GB18030ToUTF8(response.Body()))
|
||||
//logger.SugaredLogger.Infof("data:%s", data)
|
||||
datas := strutil.SplitAndTrim(data, "=", "\"")
|
||||
if len(datas) >= 2 {
|
||||
//codex := strings.Split(datas[0], "hq_str_f_")[1]
|
||||
parts := strutil.SplitAndTrim(datas[1], ",", "\"")
|
||||
//logger.SugaredLogger.Infof("parts:%s", parts)
|
||||
val, err := convertor.ToFloat(parts[1])
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
fund := &FollowedFund{
|
||||
Name: parts[0],
|
||||
Code: code,
|
||||
NetUnitValue: &val,
|
||||
NetUnitValueDate: parts[4],
|
||||
}
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
23
backend/data/fund_data_api_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCrawlFundBasic(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
db.Dao.AutoMigrate(&FundBasic{})
|
||||
api := NewFundApi()
|
||||
|
||||
//api.CrawlFundBasic("510630")
|
||||
//api.CrawlFundBasic("159688")
|
||||
//
|
||||
api.AllFund()
|
||||
}
|
||||
|
||||
func TestCrawlFundNetUnitValue(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
api := NewFundApi()
|
||||
api.CrawlFundNetUnitValue("016533")
|
||||
}
|
||||
734
backend/data/market_news_api.go
Normal file
@@ -0,0 +1,734 @@
|
||||
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"
|
||||
"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() {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
data, _ := val.Object().Value().Export()
|
||||
logger.SugaredLogger.Infof("GDP:%v", data)
|
||||
marshal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("GDP:%s", marshal)
|
||||
}
|
||||
181
backend/data/market_news_api_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"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) {
|
||||
NewMarketNewsApi().GetGDP()
|
||||
}
|
||||
@@ -1,21 +1,59 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
ai := NewDeepSeekOpenAi()
|
||||
res := ai.NewChatStream("北京文化", "sz000802")
|
||||
|
||||
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("长电科技", "sh600584", "长电科技分析和总结", nil)
|
||||
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险。调用工具函数验证", nil, tools)
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-res:
|
||||
if msg == "" {
|
||||
continue
|
||||
if len(msg) > 0 {
|
||||
t.Log(msg)
|
||||
if msg["content"] == "DONE" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Log(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTopNewsList(t *testing.T) {
|
||||
news := GetTopNewsList(30)
|
||||
t.Log(news)
|
||||
}
|
||||
|
||||
func TestSearchGuShiTongStockInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
SearchGuShiTongStockInfo("hk01810", 60)
|
||||
SearchGuShiTongStockInfo("sh600745", 60)
|
||||
SearchGuShiTongStockInfo("gb_goog", 60)
|
||||
|
||||
}
|
||||
|
||||
115
backend/data/pool.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/logger"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
// BrowserPool 浏览器池结构
|
||||
type BrowserPool struct {
|
||||
pool chan *context.Context
|
||||
mu sync.Mutex
|
||||
size int
|
||||
}
|
||||
|
||||
// NewBrowserPool 创建新的浏览器池
|
||||
func NewBrowserPool(size int) *BrowserPool {
|
||||
pool := make(chan *context.Context, size)
|
||||
for i := 0; i < size; i++ {
|
||||
path := GetConfig().BrowserPath
|
||||
crawlTimeOut := GetConfig().CrawlTimeOut
|
||||
if crawlTimeOut < 15 {
|
||||
crawlTimeOut = 30
|
||||
}
|
||||
if path != "" {
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
|
||||
ctx, _ = chromedp.NewExecAllocator(
|
||||
ctx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", true),
|
||||
chromedp.Flag("blink-settings", "imagesEnabled=false"),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
//chromedp.UserAgent(""),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
ctx, _ = chromedp.NewContext(ctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
pool <- &ctx
|
||||
}
|
||||
}
|
||||
return &BrowserPool{
|
||||
pool: pool,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
// Get 从池中获取浏览器实例
|
||||
func (pool *BrowserPool) Get() *context.Context {
|
||||
return <-pool.pool
|
||||
}
|
||||
|
||||
// Put 将浏览器实例放回池中
|
||||
func (pool *BrowserPool) Put(ctx *context.Context) {
|
||||
pool.mu.Lock()
|
||||
defer pool.mu.Unlock()
|
||||
// 检查池是否已满
|
||||
if len(pool.pool) >= pool.size {
|
||||
// 池已满,关闭并丢弃这个实例
|
||||
chromedp.Cancel(*ctx)
|
||||
return
|
||||
}
|
||||
chromedp.Cancel(*ctx)
|
||||
pool.pool <- ctx
|
||||
}
|
||||
|
||||
// Close 关闭池中的所有浏览器实例
|
||||
func (pool *BrowserPool) Close() {
|
||||
close(pool.pool)
|
||||
for ctx := range pool.pool {
|
||||
chromedp.Cancel(*ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// FetchPage 使用浏览器池获取页面内容
|
||||
func (pool *BrowserPool) FetchPage(url, waitVisible string) (string, error) {
|
||||
// 从池中获取浏览器实例
|
||||
ctx := pool.Get()
|
||||
defer pool.Put(ctx) // 使用完毕后放回池中
|
||||
var htmlContent string
|
||||
err := chromedp.Run(*ctx,
|
||||
chromedp.Navigate(url),
|
||||
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
|
||||
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
|
||||
chromedp.InnerHTML("body", &htmlContent),
|
||||
chromedp.Evaluate(`window.close()`, nil),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return htmlContent, nil
|
||||
}
|
||||
18
backend/data/pool_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPool(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
pool := NewBrowserPool(1)
|
||||
go pool.FetchPage("https://fund.eastmoney.com/016533.html", "body")
|
||||
go pool.FetchPage("https://fund.eastmoney.com/217021.html", "body")
|
||||
go pool.FetchPage("https://fund.eastmoney.com/001125.html", "body")
|
||||
|
||||
select {}
|
||||
|
||||
}
|
||||
75
backend/data/prompt_template_api.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
)
|
||||
|
||||
type PromptTemplateApi struct {
|
||||
}
|
||||
|
||||
func (t PromptTemplateApi) GetPromptTemplates(name string, promptType string) *[]models.PromptTemplate {
|
||||
var result []models.PromptTemplate
|
||||
if name != "" && promptType != "" {
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("name=? and type=?", name, promptType).Find(&result)
|
||||
}
|
||||
if name != "" && promptType == "" {
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("name=?", name).Find(&result)
|
||||
}
|
||||
if name == "" && promptType != "" {
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("type=?", promptType).Find(&result)
|
||||
}
|
||||
if name == "" && promptType == "" {
|
||||
db.Dao.Model(&models.PromptTemplate{}).Find(&result)
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
func (t PromptTemplateApi) AddPrompt(template models.PromptTemplate) string {
|
||||
var tmp models.PromptTemplate
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).First(&tmp)
|
||||
if tmp.ID == 0 {
|
||||
err := db.Dao.Model(&models.PromptTemplate{}).Create(&models.PromptTemplate{
|
||||
Content: template.Content,
|
||||
Name: template.Name,
|
||||
Type: template.Type,
|
||||
}).Error
|
||||
if err != nil {
|
||||
return "添加失败"
|
||||
} else {
|
||||
return "添加成功"
|
||||
}
|
||||
} else {
|
||||
err := db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).Updates(template).Error
|
||||
if err != nil {
|
||||
return "更新失败"
|
||||
} else {
|
||||
return "更新成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t PromptTemplateApi) DelPrompt(Id uint) string {
|
||||
template := &models.PromptTemplate{}
|
||||
db.Dao.Model(template).Where("id=?", Id).Find(template)
|
||||
if template.ID > 0 {
|
||||
err := db.Dao.Model(template).Delete(template).Error
|
||||
if err != nil {
|
||||
return "删除失败"
|
||||
} else {
|
||||
return "删除成功"
|
||||
}
|
||||
}
|
||||
return "模板信息不存在"
|
||||
}
|
||||
|
||||
func (t PromptTemplateApi) GetPromptTemplateByID(id int) string {
|
||||
prompt := &models.PromptTemplate{}
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", id).First(prompt)
|
||||
logger.SugaredLogger.Infof("GetPromptTemplateByID:%d %s", id, prompt.Content)
|
||||
return prompt.Content
|
||||
}
|
||||
func NewPromptTemplateApi() *PromptTemplateApi {
|
||||
return &PromptTemplateApi{}
|
||||
}
|
||||
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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"gorm.io/gorm"
|
||||
@@ -24,6 +25,17 @@ type Settings struct {
|
||||
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
|
||||
Prompt string `json:"prompt"`
|
||||
CheckUpdate bool `json:"checkUpdate"`
|
||||
QuestionTemplate string `json:"questionTemplate"`
|
||||
CrawlTimeOut int64 `json:"crawlTimeOut"`
|
||||
KDays int64 `json:"kDays"`
|
||||
EnableDanmu bool `json:"enableDanmu"`
|
||||
BrowserPath string `json:"browserPath"`
|
||||
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 {
|
||||
@@ -60,6 +72,16 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
"prompt": s.Config.Prompt,
|
||||
"check_update": s.Config.CheckUpdate,
|
||||
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
|
||||
"question_template": s.Config.QuestionTemplate,
|
||||
"crawl_time_out": s.Config.CrawlTimeOut,
|
||||
"k_days": s.Config.KDays,
|
||||
"enable_danmu": s.Config.EnableDanmu,
|
||||
"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)
|
||||
@@ -79,6 +101,16 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
Prompt: s.Config.Prompt,
|
||||
CheckUpdate: s.Config.CheckUpdate,
|
||||
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
|
||||
QuestionTemplate: s.Config.QuestionTemplate,
|
||||
CrawlTimeOut: s.Config.CrawlTimeOut,
|
||||
KDays: s.Config.KDays,
|
||||
EnableDanmu: s.Config.EnableDanmu,
|
||||
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 "保存成功!"
|
||||
@@ -86,5 +118,28 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
func (s SettingsApi) GetConfig() *Settings {
|
||||
var settings Settings
|
||||
db.Dao.Model(&Settings{}).First(&settings)
|
||||
|
||||
if settings.OpenAiEnable {
|
||||
if settings.OpenAiApiTimeOut <= 0 {
|
||||
settings.OpenAiApiTimeOut = 60 * 5
|
||||
}
|
||||
if settings.CrawlTimeOut <= 0 {
|
||||
settings.CrawlTimeOut = 60
|
||||
}
|
||||
if settings.KDays < 30 {
|
||||
settings.KDays = 120
|
||||
}
|
||||
}
|
||||
if settings.BrowserPath == "" {
|
||||
settings.BrowserPath, _ = CheckBrowser()
|
||||
}
|
||||
if settings.BrowserPoolSize <= 0 {
|
||||
settings.BrowserPoolSize = 1
|
||||
}
|
||||
return &settings
|
||||
}
|
||||
|
||||
func (s SettingsApi) Export() string {
|
||||
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
|
||||
return string(d)
|
||||
}
|
||||
|
||||
1
backend/data/stock_basic.json
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
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"go-stock/backend/logger"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -20,16 +23,33 @@ import (
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestGetTelegraph(t *testing.T) {
|
||||
GetTelegraphList()
|
||||
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) {
|
||||
GetFinancialReports("sz000802")
|
||||
db.Init("../../data/stock.db")
|
||||
//GetFinancialReports("sz000802", 30)
|
||||
//GetFinancialReports("hk00927", 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")
|
||||
messages := SearchStockInfo("谷歌", "telegram", 30)
|
||||
for _, message := range *messages {
|
||||
logger.SugaredLogger.Info(message)
|
||||
}
|
||||
@@ -37,11 +57,100 @@ 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) {
|
||||
SearchStockPriceInfo("sh600745")
|
||||
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)
|
||||
|
||||
}
|
||||
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 //港股
|
||||
for i := 1; i <= 592; 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)
|
||||
defer cancel()
|
||||
|
||||
text, texttime := GetRealTimeStockPriceInfo(ctx, "sh600171")
|
||||
logger.SugaredLogger.Infof("res:%s,%s", text, texttime)
|
||||
|
||||
text, texttime = GetRealTimeStockPriceInfo(ctx, "sh600438")
|
||||
logger.SugaredLogger.Infof("res:%s,%s", text, texttime)
|
||||
|
||||
texttime = strings.ReplaceAll(texttime, ")", "")
|
||||
texttime = strings.ReplaceAll(texttime, "(", "")
|
||||
parts := strings.Split(texttime, " ")
|
||||
logger.SugaredLogger.Infof("parts:%+v", parts)
|
||||
|
||||
//去除中文字符
|
||||
// 正则表达式匹配中文字符
|
||||
re := regexp.MustCompile(`\p{Han}+`)
|
||||
texttime = re.ReplaceAllString(texttime, "")
|
||||
|
||||
logger.SugaredLogger.Infof("texttime:%s", texttime)
|
||||
location, err := time.ParseInLocation("2006-01-02 15:04:05", texttime, time.Local)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("location:%s", location.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
func TestParseFullSingleStockData(t *testing.T) {
|
||||
@@ -49,7 +158,7 @@ func TestParseFullSingleStockData(t *testing.T) {
|
||||
SetHeader("Host", "hq.sinajs.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600859,sh600745"))
|
||||
Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600584,sz000938,hk01810,hk00856,gb_aapl,gb_tsla,sb873721,bj430300"))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
@@ -57,13 +166,24 @@ func TestParseFullSingleStockData(t *testing.T) {
|
||||
strs := strutil.SplitEx(data, "\n", true)
|
||||
for _, str := range strs {
|
||||
logger.SugaredLogger.Info(str)
|
||||
stockData, err := ParseFullSingleStockData(str)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("%+#v", stockData)
|
||||
}
|
||||
|
||||
result, er := ParseFullSingleStockData("var hq_str_gb_tsla = \"特斯拉,268.8472,-5.55,2025-03-04 22:52:56,-15.8028,270.9300,278.2800,268.1000,488.5400,138.8030,23618295,88214389,864751599149,2.23,120.550000,0.00,0.00,0.00,0.00,3216517037,61,0.0000,0.00,0.00,,Mar 04 09:52AM EST,284.6500,0,1,2025,6458502467.0000,0.0000,0.0000,0.0000,0.0000,284.6500\";")
|
||||
if er != nil {
|
||||
logger.SugaredLogger.Error(er.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("%+#v", result)
|
||||
}
|
||||
|
||||
func TestNewStockDataApi(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745")
|
||||
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700")
|
||||
for _, data := range *datas {
|
||||
t.Log(data)
|
||||
}
|
||||
@@ -124,7 +244,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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
}
|
||||
93
backend/data/tushare_data_api.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/logger"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/17 12:33
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
type TushareApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
}
|
||||
|
||||
func NewTushareApi(config *Settings) *TushareApi {
|
||||
return &TushareApi{
|
||||
client: resty.New(),
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDaily tushare A股日线行情
|
||||
func (receiver TushareApi) GetDaily(tsCode, startDate, endDate string, crawlTimeOut int64) string {
|
||||
//logger.SugaredLogger.Debugf("tushare daily request: ts_code=%s, start_date=%s, end_date=%s", tsCode, startDate, endDate)
|
||||
fields := "ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount"
|
||||
resp := &TushareStockBasicResponse{}
|
||||
stockType := getStockType(tsCode)
|
||||
tsCodeNEW := getTsCode(tsCode)
|
||||
//logger.SugaredLogger.Debugf("tushare daily request: %s,tsCode:%s,tsCodeNEW:%s", stockType, tsCode, tsCodeNEW)
|
||||
_, err := receiver.client.SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("content-type", "application/json").
|
||||
SetBody(&TushareRequest{
|
||||
ApiName: stockType,
|
||||
Token: receiver.config.TushareToken,
|
||||
Params: map[string]any{
|
||||
"ts_code": tsCodeNEW,
|
||||
"start_date": startDate,
|
||||
"end_date": endDate,
|
||||
},
|
||||
Fields: fields}).
|
||||
SetResult(resp).
|
||||
Post(tushareApiUrl)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return ""
|
||||
}
|
||||
res := ""
|
||||
if resp.Data.Items != nil && len(resp.Data.Items) > 0 {
|
||||
fieldsStr := slice.JoinFunc(resp.Data.Fields, ",", func(s string) string {
|
||||
return "\"" + convertor.ToString(s) + "\""
|
||||
})
|
||||
res += fieldsStr + "\n"
|
||||
for _, item := range resp.Data.Items {
|
||||
//logger.SugaredLogger.Debugf("%s", slice.Join(item, ","))
|
||||
t := slice.JoinFunc(item, ",", func(s any) any {
|
||||
return "\"" + convertor.ToString(s) + "\""
|
||||
})
|
||||
res += t + "\n"
|
||||
}
|
||||
}
|
||||
//logger.SugaredLogger.Debugf("tushare response: %s", res)
|
||||
return res
|
||||
}
|
||||
|
||||
func getTsCode(code string) any {
|
||||
if strutil.HasPrefixAny(code, []string{"US", "us", "gb_"}) {
|
||||
code = strings.Replace(code, "gb_", "", 1)
|
||||
code = strings.Replace(code, "us", "", 1)
|
||||
return code
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func getStockType(code string) string {
|
||||
if strutil.HasSuffixAny(code, []string{"SZ", "SH", "sh", "sz"}) {
|
||||
return "daily"
|
||||
}
|
||||
if strutil.HasSuffixAny(code, []string{"HK", "hk"}) {
|
||||
return "hk_daily"
|
||||
}
|
||||
if strutil.HasPrefixAny(code, []string{"US", "us", "gb_"}) {
|
||||
return "us_daily"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
29
backend/data/tushare_data_api_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/17 12:44
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
func TestGetDaily(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
tushareApi := NewTushareApi(GetConfig())
|
||||
res := tushareApi.GetDaily("00927.HK", "20250101", "20250217", 30)
|
||||
t.Log(res)
|
||||
|
||||
}
|
||||
|
||||
func TestGetUSDaily(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
tushareApi := NewTushareApi(GetConfig())
|
||||
|
||||
res := tushareApi.GetDaily("gb_AAPL", "20250101", "20250217", 30)
|
||||
t.Log(res)
|
||||
|
||||
//
|
||||
|
||||
}
|
||||
111
backend/data/utils.go
Normal file
47
backend/data/utils_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"go-stock/backend/logger"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRemoveNonPrintable tests the RemoveAllBlankChar function.
|
||||
func TestRemoveNonPrintable(t *testing.T) {
|
||||
//tests := []struct {
|
||||
// input string
|
||||
// expected string
|
||||
//}{
|
||||
// {"新 希 望", "新希望"},
|
||||
// {"", ""},
|
||||
// {"Hello, World!", "Hello, World!"},
|
||||
// {"\x00\x01\x02", ""},
|
||||
// {"Hello\x00World", "HelloWorld"},
|
||||
// {"\x1F\x20\x7E\x7F", " \x7E"},
|
||||
//}
|
||||
|
||||
//for _, test := range tests {
|
||||
// actual := RemoveAllBlankChar(test.input)
|
||||
// if actual != test.expected {
|
||||
// t.Errorf("RemoveAllBlankChar(%q) = %q; expected %q", test.input, actual, test.expected)
|
||||
// }
|
||||
//}
|
||||
txt := "新 希 望"
|
||||
txt2 := RemoveAllBlankChar(txt)
|
||||
logger.SugaredLogger.Infof("RemoveAllBlankChar(%s)", txt2)
|
||||
logger.SugaredLogger.Infof("RemoveAllBlankChar(%s)", txt)
|
||||
|
||||
}
|
||||
|
||||
func TestConvertStockCodeToTushareCode(t *testing.T) {
|
||||
logger.SugaredLogger.Infof("ConvertStockCodeToTushareCode(%s)", ConvertStockCodeToTushareCode("sz000802"))
|
||||
logger.SugaredLogger.Infof("ConvertTushareCodeToStockCode(%s)", ConvertTushareCodeToStockCode("000802.SZ"))
|
||||
}
|
||||
func TestReplaceSensitiveWords(t *testing.T) {
|
||||
txt := "新 希 望习近平"
|
||||
txt2 := ReplaceSensitiveWords(txt)
|
||||
logger.SugaredLogger.Infof("ReplaceSensitiveWords(%s)", txt2)
|
||||
|
||||
os.WriteFile("words.txt", []byte(slice.Join(SensitiveWords, "\n")), 0644)
|
||||
}
|
||||
@@ -135,8 +135,11 @@ type Commit struct {
|
||||
|
||||
type AIResponseResult struct {
|
||||
gorm.Model
|
||||
ChatId string `json:"chatId"`
|
||||
ModelName string `json:"modelName"`
|
||||
StockCode string `json:"stockCode"`
|
||||
StockName string `json:"stockName"`
|
||||
Question string `json:"question"`
|
||||
Content string `json:"content"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
@@ -147,13 +150,236 @@ func (receiver AIResponseResult) TableName() string {
|
||||
|
||||
type VersionInfo struct {
|
||||
gorm.Model
|
||||
Version string `json:"version"`
|
||||
Content string `json:"content"`
|
||||
Icon string `json:"icon"`
|
||||
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 {
|
||||
return "version_info"
|
||||
}
|
||||
|
||||
type StockInfoHK struct {
|
||||
gorm.Model
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"fullName"`
|
||||
EName string `json:"eName"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver StockInfoHK) TableName() string {
|
||||
return "stock_base_info_hk"
|
||||
}
|
||||
|
||||
type StockInfoUS struct {
|
||||
gorm.Model
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"fullName"`
|
||||
EName string `json:"eName"`
|
||||
Exchange string `json:"exchange"`
|
||||
Type string `json:"type"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver StockInfoUS) TableName() string {
|
||||
return "stock_base_info_us"
|
||||
}
|
||||
|
||||
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 {
|
||||
ID int `gorm:"primarykey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (p PromptTemplate) TableName() string {
|
||||
return "prompt_templates"
|
||||
}
|
||||
|
||||
type Prompt struct {
|
||||
ID int `json:"ID"`
|
||||
Name string `json:"name"`
|
||||
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:"第三产业同比增长(%)"`
|
||||
}
|
||||
|
||||
49
backend/models/models_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/22 16:09
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type StockInfoHKResp struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
StockInfos *[]StockInfoData `json:"data"`
|
||||
}
|
||||
|
||||
type StockInfoData struct {
|
||||
C string `json:"c"`
|
||||
N string `json:"n"`
|
||||
T string `json:"t"`
|
||||
E string `json:"e"`
|
||||
}
|
||||
|
||||
func TestStockInfoHK(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
db.Dao.AutoMigrate(&StockInfoHK{})
|
||||
bs, _ := os.ReadFile("../../build/hk.json")
|
||||
v := &StockInfoHKResp{}
|
||||
err := json.Unmarshal(bs, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hks := &[]StockInfoHK{}
|
||||
for i, data := range *v.StockInfos {
|
||||
logger.SugaredLogger.Infof("第%d条数据: %+v", i, data)
|
||||
hk := &StockInfoHK{
|
||||
Code: strutil.PadStart(data.C, 5, "0") + ".HK",
|
||||
EName: data.N,
|
||||
}
|
||||
*hks = append(*hks, *hk)
|
||||
}
|
||||
db.Dao.Create(&hks)
|
||||
|
||||
}
|
||||
262
backend/util/struct_to_markdown.go
Normal file
@@ -0,0 +1,262 @@
|
||||
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 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
@@ -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))
|
||||
}
|
||||
15192
build/hk.json
Normal file
BIN
build/screenshot/img13.png
Normal file
|
After Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 170 KiB |
BIN
build/screenshot/img_13.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
build/screenshot/img_14.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
build/screenshot/img_4.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
build/screenshot/扫码_搜索联合传播样式-白色版.png
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
48622
build/stock_base_info_hk.json
Normal file
189906
build/stock_base_info_us.json
Normal file
BIN
build/wx.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>wails-naive-demo</title>
|
||||
<title>go-stock:AI赋能股票分析</title>
|
||||
<link href="./src/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
1137
frontend/package-lock.json
generated
@@ -9,17 +9,37 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"md-editor-v3": "^5.2.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@vavt/cm-extension": "^1.8.0",
|
||||
"@vavt/v3-extension": "^3.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"echarts": "^5.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"md-editor-v3": "^5.2.3",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.5.0"
|
||||
"vue-router": "^4.5.0",
|
||||
"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.6"
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"keywords": ["AI赋能股票分析","go-stock"],
|
||||
"keywords": [
|
||||
"AI赋能股票分析",
|
||||
"go-stock"
|
||||
],
|
||||
"author": "spark"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
39a415166f03acc0270e24443a9e2445
|
||||
2d63c3a999d797889c01d6c96451b197
|
||||
@@ -1,29 +1,56 @@
|
||||
<script setup>
|
||||
import stockInfo from './components/stock.vue'
|
||||
import {
|
||||
EventsEmit,
|
||||
EventsOff,
|
||||
EventsOn,
|
||||
Quit,
|
||||
WindowFullscreen, WindowGetPosition,
|
||||
WindowFullscreen,
|
||||
WindowHide,
|
||||
WindowIsFullscreen, WindowSetPosition,
|
||||
WindowUnfullscreen
|
||||
} from '../wailsjs/runtime'
|
||||
import {h, ref} from "vue";
|
||||
import { RouterLink } from 'vue-router'
|
||||
import {darkTheme, NIcon, NText,} from 'naive-ui'
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {RouterLink, useRouter} from 'vue-router'
|
||||
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui'
|
||||
import {
|
||||
SettingsOutline,
|
||||
AlarmOutline,
|
||||
AnalyticsOutline,
|
||||
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
|
||||
ExpandOutline, Flag,
|
||||
Flame, FlameSharp, InformationOutline,
|
||||
LogoGithub,
|
||||
NewspaperOutline,
|
||||
NewspaperSharp, Notifications,
|
||||
PowerOutline, Pulse,
|
||||
ReorderTwoOutline,
|
||||
ExpandOutline,
|
||||
RefreshOutline, PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline,
|
||||
SettingsOutline, Skull, SkullOutline, SkullSharp,
|
||||
SparklesOutline,
|
||||
StarOutline,
|
||||
Wallet, WarningOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
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 content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
|
||||
|
||||
|
||||
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('stock')
|
||||
const containerRef= ref({})
|
||||
const realtimeProfit= ref(0)
|
||||
const telegraph= ref([])
|
||||
const containerRef = ref({})
|
||||
const realtimeProfit = ref(0)
|
||||
const telegraph = ref([])
|
||||
const groupList = ref([])
|
||||
const menuOptions = ref([
|
||||
{
|
||||
label: () =>
|
||||
@@ -32,21 +59,344 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'stock',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
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+"¥"}),
|
||||
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),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'fund',
|
||||
query: {
|
||||
name: '基金自选',
|
||||
},
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'fund'
|
||||
},
|
||||
},
|
||||
{default: () => '基金自选',}
|
||||
),
|
||||
show: enableFund.value,
|
||||
key: 'fund',
|
||||
icon: renderIcon(SparklesOutline),
|
||||
children: [
|
||||
{
|
||||
label: () => h(NText, {type: realtimeProfit.value > 0 ? 'error' : 'success'}, {default: () => '功能完善中!'}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(WalletOutline),
|
||||
icon: renderIcon(AlarmOutline),
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -57,12 +407,15 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'settings',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
}
|
||||
query: {
|
||||
name:"设置",
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'settings'
|
||||
},
|
||||
}
|
||||
},
|
||||
{ default: () => '设置' }
|
||||
{default: () => '设置'}
|
||||
),
|
||||
key: 'settings',
|
||||
icon: renderIcon(SettingsOutline),
|
||||
@@ -74,137 +427,287 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'about',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
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),
|
||||
},
|
||||
// {
|
||||
// label: ()=> h("a", {
|
||||
// href: 'javascript:void(0)',
|
||||
// style: 'cursor: move;',
|
||||
// onClick: toggleStartMoveWindow,
|
||||
// }, { default: () => '移动' }),
|
||||
// key: 'move',
|
||||
// icon: renderIcon(MoveOutline),
|
||||
// },
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: 'javascript:void(0)',
|
||||
style: 'cursor: move;',
|
||||
onClick: toggleStartMoveWindow,
|
||||
}, { default: () => '移动' }),
|
||||
key: 'move',
|
||||
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) })
|
||||
}
|
||||
function toggleFullscreen(e) {
|
||||
//console.log(e)
|
||||
if (isFullscreen.value) {
|
||||
WindowUnfullscreen()
|
||||
//e.target.innerHTML = '全屏'
|
||||
} else {
|
||||
WindowFullscreen()
|
||||
// e.target.innerHTML = '取消全屏'
|
||||
}
|
||||
isFullscreen.value=!isFullscreen.value
|
||||
}
|
||||
const drag = ref(false)
|
||||
const lastPos= ref({x:0,y:0})
|
||||
function toggleStartMoveWindow(e) {
|
||||
drag.value=!drag.value
|
||||
lastPos.value={x:e.clientX,y:e.clientY}
|
||||
}
|
||||
function dragstart(e) {
|
||||
if (drag.value) {
|
||||
let x=e.clientX-lastPos.value.x
|
||||
let y=e.clientY-lastPos.value.y
|
||||
WindowGetPosition().then((pos) => {
|
||||
WindowSetPosition(pos.x+x,pos.y+y)
|
||||
})
|
||||
}
|
||||
}
|
||||
window.addEventListener('mousemove', dragstart)
|
||||
|
||||
EventsOn("realtime_profit",(data)=>{
|
||||
realtimeProfit.value=data
|
||||
function renderIcon(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
|
||||
}
|
||||
|
||||
// const drag = ref(false)
|
||||
// const lastPos= ref({x:0,y:0})
|
||||
// function toggleStartMoveWindow(e) {
|
||||
// drag.value=!drag.value
|
||||
// lastPos.value={x:e.clientX,y:e.clientY}
|
||||
// }
|
||||
// function dragstart(e) {
|
||||
// if (drag.value) {
|
||||
// let x=e.clientX-lastPos.value.x
|
||||
// let y=e.clientY-lastPos.value.y
|
||||
// WindowGetPosition().then((pos) => {
|
||||
// WindowSetPosition(pos.x+x,pos.y+y)
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// window.addEventListener('mousemove', dragstart)
|
||||
|
||||
EventsOn("realtime_profit", (data) => {
|
||||
realtimeProfit.value = data
|
||||
})
|
||||
EventsOn("telegraph",(data)=>{
|
||||
telegraph.value=data
|
||||
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) {
|
||||
// 将错误信息发送给后端
|
||||
EventsEmit("frontendError", {
|
||||
page: "App.vue",
|
||||
message: msg,
|
||||
source: source,
|
||||
lineno: lineno,
|
||||
colno: colno,
|
||||
error: error ? error.stack : null,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
GetVersionInfo().then(result => {
|
||||
if(result.officialStatement){
|
||||
content.value = result.officialStatement+"\n\n"+content.value
|
||||
}
|
||||
})
|
||||
|
||||
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(NText,{type:"error"}, { 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(NText,{type:"info"}, { default: () => data.content }),
|
||||
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
|
||||
duration:1000*30 ,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
|
||||
|
||||
<n-config-provider :theme="darkTheme" ref="containerRef">
|
||||
<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-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-marquee :speed="120" >
|
||||
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
|
||||
{{item}}
|
||||
</n-tag>
|
||||
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
|
||||
</n-marquee>
|
||||
</n-gi>
|
||||
<n-gi style="padding-bottom: 70px;padding-top: 5px">
|
||||
<RouterView />
|
||||
</n-gi>
|
||||
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%">
|
||||
<n-card size="small">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
responsive
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
</n-flex>
|
||||
</n-watermark>
|
||||
</n-modal-provider>
|
||||
<n-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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;" :style="{height:chartHeight+'px'}"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
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
@@ -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 - 170px)">
|
||||
<n-gi :span="4">
|
||||
<n-list bordered style="text-align: left;" hoverable clickable>
|
||||
<n-scrollbar style="max-height: calc(100vh - 170px);">
|
||||
<n-list-item v-for="item in hotStrategy" :key="item.rank" @click="DoSearch(item.question)">
|
||||
<n-ellipsis line-clamp="1" :tooltip="true">
|
||||
<n-tag size="small" :bordered="false" type="info">#{{ item.rank }}</n-tag>
|
||||
<n-text type="warning">{{ item.question }}</n-text>
|
||||
<template #tooltip>
|
||||
<div style="text-align: center;max-width: 180px">
|
||||
<n-text type="warning">{{ item.question }}</n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
</n-list-item>
|
||||
</n-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>
|
||||
<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">
|
||||
|
||||
<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 - 250px)'"
|
||||
size="medium"
|
||||
:columns="columns"
|
||||
:data="dataList"
|
||||
:pagination="{pageSize: 9}"
|
||||
: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}` })
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<n-text>共找到
|
||||
<n-tag type="info" :bordered="false">{{ dataList.length }}</n-tag>
|
||||
只股
|
||||
</n-text>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
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
@@ -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>
|
||||
@@ -1,35 +1,131 @@
|
||||
<script setup>
|
||||
import { MdPreview } from 'md-editor-v3';
|
||||
// import { MdPreview } from 'md-editor-v3';
|
||||
// preview.css相比style.css少了编辑器那部分样式
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {GetVersionInfo} from "../../wailsjs/go/main/App";
|
||||
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
|
||||
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 = '关于软件';
|
||||
GetVersionInfo().then((res) => {
|
||||
updateLog.value = res.content;
|
||||
versionInfo.value = res.version;
|
||||
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) => {
|
||||
const githubTimeStr = msg.published_at;
|
||||
// 创建一个 Date 对象
|
||||
const utcDate = new Date(githubTimeStr);
|
||||
// 获取本地时间
|
||||
const date = new Date(utcDate.getTime());
|
||||
const year = date.getFullYear();
|
||||
// getMonth 返回值是 0 - 11,所以要加 1
|
||||
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');
|
||||
|
||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
|
||||
//console.log("GitHub UTC 时间:", utcDate);
|
||||
//console.log("转换后的本地时间:", formattedDate);
|
||||
notify.info({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: false,
|
||||
src: icon.value
|
||||
}),
|
||||
title: '发现新版本: ' + msg.tag_name,
|
||||
content: () => {
|
||||
//return h(MdPreview, {theme:'dark',modelValue:msg.commit?.message}, null)
|
||||
return h('div', {
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
'font-size': '14px',
|
||||
}
|
||||
}, { default: () => msg.commit?.message })
|
||||
},
|
||||
duration: 5000,
|
||||
meta: "发布时间:"+formattedDate,
|
||||
action: () => {
|
||||
return h(NButton, {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(msg.html_url)
|
||||
break
|
||||
default :
|
||||
OpenURL(msg.html_url)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}, { default: () => '查看' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider>
|
||||
<n-layout>
|
||||
<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 >
|
||||
<h1>关于软件</h1>
|
||||
<n-image width="100" :src="icon" />
|
||||
<h1>go-stock <n-tag size="small" round>{{versionInfo}}</n-tag></h1>
|
||||
<h1>
|
||||
<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-gradient-text type="warning" v-if="vipLevel" >vip到期时间:{{vipEndTime}}</n-gradient-text>
|
||||
<n-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p>自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
|
||||
<p>目前已支持A股,港股,美股,未来计划加入基金,ETF等支持</p>
|
||||
<p>支持DeepSeek,OpenAI, Ollama,LMStudio,AnythingLLM,<a href="https://cloud.siliconflow.cn/i/foufCerk" target="_blank">硅基流动</a>,<a href="https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ" target="_blank">火山方舟</a>,阿里云百炼等平台或模型</p>
|
||||
<p>
|
||||
<i style="color: crimson">本软件仅供学习研究目的,AI分析结果仅供参考,本软件不提供任何投资建议或决策,风险自担!</i>
|
||||
</p>
|
||||
<p>
|
||||
欢迎点赞GitHub:<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">go-stock</a><n-divider vertical />
|
||||
@@ -38,20 +134,48 @@ onMounted(() => {
|
||||
<a href="https://github.com/ArvinLovegood/go-stock/releases" target="_blank">Releases</a><n-divider vertical />
|
||||
</p>
|
||||
<p v-if="updateLog">更新说明:{{updateLog}}</p>
|
||||
<p>项目社区:<a href="https://go-stock.sparkmemory.top/" target="_blank">https://go-stock.sparkmemory.top/</a></p>
|
||||
<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-card>
|
||||
<!-- 关于作者 -->
|
||||
<n-card size="large">
|
||||
<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>
|
||||
<!-- <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>
|
||||
<p>
|
||||
邮箱:<a href="mailto:sparkmemory@163.com">sparkmemory@163.com</a><n-divider vertical />
|
||||
QQ: 506808970<n-divider vertical />
|
||||
微信:ArvinLovegood</p><n-divider vertical />
|
||||
<p>一个热爱编程的小白,欢迎关注我的Github/微信公众号</p>
|
||||
<n-image width="300" :src="wxgzh" />
|
||||
<p>开源不易,如果觉得好用,可以请作者喝杯咖啡。</p>
|
||||
<n-flex justify="center">
|
||||
<n-image width="200" :src="alipay" />
|
||||
<n-image width="200" :src="wxpay" />
|
||||
</n-flex>
|
||||
</n-space>
|
||||
<n-divider title-placement="center">鸣谢</n-divider>
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
@@ -61,6 +185,8 @@ onMounted(() => {
|
||||
</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 />
|
||||
@@ -73,10 +199,49 @@ onMounted(() => {
|
||||
<a href="https://github.com/tusen-ai/naive-ui" target="_blank">NaiveUI</a><n-divider vertical />
|
||||
</p>
|
||||
</div>
|
||||
<n-divider title-placement="center">关于版权和技术支持申明</n-divider>
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p style="color: #FAA04A">如有问题,请先查看项目文档,如果问题依然存在,请优先加群(491605333)咨询。</p>
|
||||
<p>
|
||||
如需软件商业授权或定制开发,请联系作者微信(备注 商业咨询):ArvinLovegood
|
||||
</p>
|
||||
<n-divider/>
|
||||
<p>
|
||||
本软件基于开源技术构建,使用Wails、NaiveUI、Vue等开源项目。技术上如有问题,可以先向对应的开源社区请求帮助。
|
||||
</p>
|
||||
<p>
|
||||
开源不易,本人精力和时间有限,如确实需要一对一技术支持,<i style="color: crimson">请先赞助!</i>联系微信(备注 技术支持):ArvinLovegood
|
||||
</p>
|
||||
<p style="color: #FAA04A">*加微信或者QQ时,请先备注或留言需求(如:<a href="#support">技术支持</a>,功能建议,商业咨询等,否则会被忽略)</p>
|
||||
<n-table id="support">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>技术支持方式</n-th><n-th>赞助(元)</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr>
|
||||
<n-td>
|
||||
加 QQ:506808970,微信:ArvinLovegood
|
||||
</n-td>
|
||||
<n-td>
|
||||
100/次
|
||||
</n-td>
|
||||
</n-tr>
|
||||
<n-tr>
|
||||
<n-td>
|
||||
长期技术支持(不限次数,新功能优先体验等)
|
||||
</n-td>
|
||||
<n-td>
|
||||
5000
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</div>
|
||||
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-layout>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
289
frontend/src/components/fund.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<script setup>
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from "vue";
|
||||
import {Add, ChatboxOutline} from "@vicons/ionicons5";
|
||||
import {NButton, NEllipsis, NText, useMessage} from "naive-ui";
|
||||
import {
|
||||
FollowFund,
|
||||
GetConfig,
|
||||
GetFollowedFund,
|
||||
GetfundList,
|
||||
GetVersionInfo, OpenURL,
|
||||
UnFollowFund
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import vueDanmaku from 'vue3-danmaku'
|
||||
|
||||
const danmus = ref([])
|
||||
const ws = ref(null)
|
||||
const icon = ref(null)
|
||||
const message = useMessage()
|
||||
const modalShow = ref(false)
|
||||
const data = reactive({
|
||||
modelName:"",
|
||||
chatId: "",
|
||||
question:"",
|
||||
name: "",
|
||||
code: "",
|
||||
fenshiURL:"",
|
||||
kURL:"",
|
||||
fullscreen: false,
|
||||
airesult: "",
|
||||
openAiEnable: false,
|
||||
loading: true,
|
||||
enableDanmu: false,
|
||||
})
|
||||
|
||||
const followList=ref([])
|
||||
const options=ref([])
|
||||
const ticker=ref({})
|
||||
|
||||
onBeforeMount(()=>{
|
||||
GetConfig().then(result => {
|
||||
if (result.openAiEnable) {
|
||||
data.openAiEnable = true
|
||||
}
|
||||
if (result.enableDanmu) {
|
||||
data.enableDanmu = true
|
||||
}
|
||||
})
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
GetVersionInfo().then((res) => {
|
||||
icon.value = res.icon;
|
||||
});
|
||||
// 创建 WebSocket 连接
|
||||
ws.value = new WebSocket('ws://8.134.249.145:16688/ws'); // 替换为你的 WebSocket 服务器地址
|
||||
//ws.value = new WebSocket('ws://localhost:16688/ws'); // 替换为你的 WebSocket 服务器地址
|
||||
|
||||
ws.value.onopen = () => {
|
||||
//console.log('WebSocket 连接已打开');
|
||||
};
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
if(data.enableDanmu){
|
||||
danmus.value.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
console.error('WebSocket 错误:', error);
|
||||
};
|
||||
|
||||
ws.value.onclose = () => {
|
||||
//console.log('WebSocket 连接已关闭');
|
||||
};
|
||||
|
||||
ticker.value=setInterval(() => {
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
}, 1000*60)
|
||||
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(ticker.value)
|
||||
ws.value.close()
|
||||
message.destroyAll()
|
||||
})
|
||||
|
||||
|
||||
|
||||
function SendDanmu(){
|
||||
ws.value.send(data.name)
|
||||
}
|
||||
function AddFund(){
|
||||
FollowFund(data.code).then(result=>{
|
||||
if(result){
|
||||
message.success("关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
function unFollow(code){
|
||||
UnFollowFund(code).then(result=>{
|
||||
if(result){
|
||||
message.success("取消关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getFundList(value){
|
||||
GetfundList(value).then(result=>{
|
||||
options.value=[]
|
||||
result.forEach(item=>{
|
||||
options.value.push({
|
||||
label: item.name+" ["+item.code+"]",
|
||||
value: item.code,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
function onSelectFund(value){
|
||||
data.code=value
|
||||
blinkBorder(value)
|
||||
}
|
||||
function formatterTitle(title){
|
||||
return () => h(NEllipsis,{
|
||||
style: {
|
||||
'font-size': '16px',
|
||||
'max-width': '180px',
|
||||
},
|
||||
},{default: () => 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://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)
|
||||
}
|
||||
|
||||
function newchart(code,name){
|
||||
modalShow.value=true
|
||||
data.name=name
|
||||
data.code=code
|
||||
data.fenshiURL='https://image.sinajs.cn/newchart/v5/fund/nav/ss/'+code+'.gif'+"?t="+Date.now()
|
||||
}
|
||||
|
||||
function blinkBorder(findId){
|
||||
// 获取要滚动到的元素
|
||||
const element = document.getElementById(findId);
|
||||
if (element) {
|
||||
// 滚动到该元素
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
const pelement = document.getElementById(findId +'_gi');
|
||||
if(pelement){
|
||||
// 添加闪烁效果
|
||||
pelement.classList.add('blink-border');
|
||||
// 3秒后移除闪烁效果
|
||||
setTimeout(() => {
|
||||
pelement.classList.remove('blink-border');
|
||||
}, 1000*5);
|
||||
}else{
|
||||
console.error(`Element with ID ${findId}_gi not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vue-danmaku v-model:danmus="danmus" useSlot style="height:100px; width:100%;z-index: 9;position:absolute; top: 400px; pointer-events: none;" >
|
||||
<template v-slot:dm="{ index, danmu }">
|
||||
<n-gradient-text type="info">
|
||||
<n-icon :component="ChatboxOutline"/>{{ danmu }}
|
||||
</n-gradient-text>
|
||||
</template>
|
||||
</vue-danmaku>
|
||||
<n-flex justify="start" >
|
||||
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
|
||||
<n-gi :id="info.code+'_gi'" v-for="info in followList" style="margin-left: 2px" >
|
||||
<n-card :id="info.code" :title="formatterTitle(info.name)">
|
||||
<template #header-extra>
|
||||
<n-tag size="small" :bordered="false" type="info">{{info.code}}</n-tag>
|
||||
<n-tag size="small" :bordered="false" type="success" @click="unFollow(info.code)"> 取消关注</n-tag>
|
||||
</template>
|
||||
<n-flex>
|
||||
<n-text size="small" :type="info.netEstimatedRate>0?'error':'success'" :bordered="false" v-if="info.netEstimatedUnit">
|
||||
估算净值:{{info.netEstimatedUnit}}
|
||||
{{info.netEstimatedRate}} %
|
||||
({{info.netEstimatedUnitTime}})</n-text>
|
||||
<br>
|
||||
<n-text size="small" :type="info.netEstimatedRate>0?'error':'success'" :bordered="false" v-if="info.netUnitValue">
|
||||
单位净值:{{info.netUnitValue}} ({{info.netUnitValueDate}})</n-text>
|
||||
</n-flex>
|
||||
<n-flex justify="start" style="margin-top: 10px">
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth1>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth1">近一月:{{info.fundBasic.netGrowth1}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth3>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth3">近三月:{{info.fundBasic.netGrowth3}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth6>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth6">近六月:{{info.fundBasic.netGrowth6}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth12>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth12">近一年:{{info.fundBasic.netGrowth12}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth36>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth36">近三年:{{info.fundBasic.netGrowth36}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth60>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth60">近五年:{{info.fundBasic.netGrowth60}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowthYTD>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowthYTD" >今年来:{{info.fundBasic.netGrowthYTD}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowthAll>0?'error':'success'" :bordered="false" >成立来:{{info.fundBasic.netGrowthAll}}%</n-tag>
|
||||
</n-flex>
|
||||
<template #footer>
|
||||
<n-flex justify="space-between">
|
||||
<n-tag size="small" :bordered="false" type="warning"> {{info.fundBasic.type}}</n-tag>
|
||||
<n-tag size="small" :bordered="false" type="info"> {{info.fundBasic.company}}:{{info.fundBasic.manager}}</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #action>
|
||||
<n-flex justify="end">
|
||||
<n-button size="tiny" type="error" @click="newchart(info.code,info.name)"> 走势 </n-button>
|
||||
<n-button size="tiny" type="warning" @click="search(info.code,info.name)"> 详情 </n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
|
||||
<n-modal v-model:show="modalShow" :title="data.name" style="width: 400px" :preset="'card'">
|
||||
<n-image :src="data.fenshiURL" />
|
||||
</n-modal>
|
||||
|
||||
<div style="position: fixed;bottom: 18px;right:5px;z-index: 10;width: 400px">
|
||||
<n-input-group >
|
||||
<n-auto-complete v-model:value="data.name"
|
||||
:input-props="{
|
||||
autocomplete: 'disabled',
|
||||
}"
|
||||
:options="options"
|
||||
placeholder="基金名称/代码/弹幕"
|
||||
clearable @update-value="getFundList" :on-select="onSelectFund"/>
|
||||
<n-button type="primary" @click="AddFund" >
|
||||
<n-icon :component="Add"/>
|
||||
关注
|
||||
</n-button>
|
||||
<n-button type="info" @click="SendDanmu" v-if="data.enableDanmu" >
|
||||
<n-icon :component="ChatboxOutline"/>
|
||||
发送弹幕
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 添加闪烁效果的CSS类 */
|
||||
.blink-border {
|
||||
animation: blink-border 1s linear infinite;
|
||||
border: 4px solid transparent;
|
||||
}
|
||||
|
||||
@keyframes blink-border {
|
||||
0% {
|
||||
border-color: red;
|
||||
}
|
||||
50% {
|
||||
border-color: transparent;
|
||||
}
|
||||
100% {
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
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>
|
||||
676
frontend/src/components/market.vue
Normal file
@@ -0,0 +1,676 @@
|
||||
<script setup>
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, 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 style="--wails-draggable:drag">
|
||||
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
|
||||
<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="指标行情" style="--wails-dragable:no-drag">
|
||||
<n-tabs type="segment" animated>
|
||||
<n-tab-pane name="科创50" tab="科创50">
|
||||
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
|
||||
: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
@@ -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
@@ -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
@@ -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,9 +1,17 @@
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
|
||||
import {useMessage} from "naive-ui";
|
||||
import {data} from "../../wailsjs/go/models";
|
||||
import {h, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {
|
||||
AddPrompt, DelPrompt,
|
||||
ExportConfig,
|
||||
GetConfig,
|
||||
GetPromptTemplates,
|
||||
SendDingDingMessageByType,
|
||||
UpdateConfig,CheckSponsorCode
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import {NTag, useMessage} from "naive-ui";
|
||||
import {data, models} from "../../wailsjs/go/models";
|
||||
import {EventsEmit} from "../../wailsjs/runtime";
|
||||
const message = useMessage()
|
||||
|
||||
const formRef = ref(null)
|
||||
@@ -27,10 +35,20 @@ const formValue = ref({
|
||||
temperature: 0.1,
|
||||
maxTokens: 1024,
|
||||
prompt:"",
|
||||
timeout: 5
|
||||
timeout: 5,
|
||||
questionTemplate: "{{stockName}}分析和总结",
|
||||
crawlTimeOut:30,
|
||||
kDays:30,
|
||||
},
|
||||
enableDanmu:false,
|
||||
browserPath: '',
|
||||
enableNews:false,
|
||||
darkTheme:true,
|
||||
enableFund:false,
|
||||
enablePushNews:false,
|
||||
sponsorCode:"",
|
||||
})
|
||||
|
||||
const promptTemplates=ref([])
|
||||
onMounted(()=>{
|
||||
GetConfig().then(res=>{
|
||||
formValue.value.ID = res.ID
|
||||
@@ -52,15 +70,35 @@ onMounted(()=>{
|
||||
temperature:res.openAiTemperature,
|
||||
maxTokens:res.openAiMaxTokens,
|
||||
prompt:res.prompt,
|
||||
timeout:res.openAiApiTimeOut
|
||||
timeout:res.openAiApiTimeOut,
|
||||
questionTemplate:res.questionTemplate?res.questionTemplate:'{{stockName}}分析和总结',
|
||||
crawlTimeOut:res.crawlTimeOut,
|
||||
kDays:res.kDays,
|
||||
}
|
||||
console.log(res)
|
||||
formValue.value.enableDanmu = res.enableDanmu
|
||||
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)
|
||||
})
|
||||
//message.info("加载完成")
|
||||
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
message.destroyAll()
|
||||
})
|
||||
|
||||
|
||||
function saveConfig(){
|
||||
|
||||
let config= new data.Settings({
|
||||
ID:formValue.value.ID,
|
||||
dingPushEnable:formValue.value.dingPush.enable,
|
||||
@@ -76,13 +114,37 @@ function saveConfig(){
|
||||
openAiTemperature:formValue.value.openAI.temperature,
|
||||
tushareToken:formValue.value.tushareToken,
|
||||
prompt:formValue.value.openAI.prompt,
|
||||
openAiApiTimeOut:formValue.value.openAI.timeout
|
||||
openAiApiTimeOut:formValue.value.openAI.timeout,
|
||||
questionTemplate:formValue.value.openAI.questionTemplate,
|
||||
crawlTimeOut:formValue.value.openAI.crawlTimeOut,
|
||||
kDays:formValue.value.openAI.kDays,
|
||||
enableDanmu:formValue.value.enableDanmu,
|
||||
browserPath:formValue.value.browserPath,
|
||||
enableNews:formValue.value.enableNews,
|
||||
darkTheme:formValue.value.darkTheme,
|
||||
enableFund:formValue.value.enableFund,
|
||||
enablePushNews:formValue.value.enablePushNews,
|
||||
sponsorCode:formValue.value.sponsorCode
|
||||
})
|
||||
|
||||
//console.log("Settings",config)
|
||||
UpdateConfig(config).then(res=>{
|
||||
message.success(res)
|
||||
})
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -106,72 +168,221 @@ function sendTestNotice(){
|
||||
message.info(res)
|
||||
})
|
||||
}
|
||||
|
||||
function exportConfig(){
|
||||
ExportConfig().then(res=>{
|
||||
message.info(res)
|
||||
})
|
||||
}
|
||||
|
||||
function importConfig(){
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = (e) => {
|
||||
let file = e.target.files[0];
|
||||
let reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
let config = JSON.parse(e.target.result);
|
||||
//console.log(config)
|
||||
formValue.value.ID = config.ID
|
||||
formValue.value.tushareToken = config.tushareToken
|
||||
formValue.value.dingPush = {
|
||||
enable:config.dingPushEnable,
|
||||
dingRobot:config.dingRobot
|
||||
}
|
||||
formValue.value.localPush = {
|
||||
enable:config.localPushEnable,
|
||||
}
|
||||
formValue.value.updateBasicInfoOnStart = config.updateBasicInfoOnStart
|
||||
formValue.value.refreshInterval = config.refreshInterval
|
||||
formValue.value.openAI = {
|
||||
enable:config.openAiEnable,
|
||||
baseUrl: config.openAiBaseUrl,
|
||||
apiKey:config.openAiApiKey,
|
||||
model:config.openAiModelName,
|
||||
temperature:config.openAiTemperature,
|
||||
maxTokens:config.openAiMaxTokens,
|
||||
prompt:config.prompt,
|
||||
timeout:config.openAiApiTimeOut,
|
||||
questionTemplate:config.questionTemplate,
|
||||
crawlTimeOut:config.crawlTimeOut,
|
||||
kDays:config.kDays
|
||||
}
|
||||
formValue.value.enableDanmu = config.enableDanmu
|
||||
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);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
|
||||
window.onerror = function (event, source, lineno, colno, error) {
|
||||
//console.log(event, source, lineno, colno, error)
|
||||
// 将错误信息发送给后端
|
||||
EventsEmit("frontendError", {
|
||||
page: "settings.vue",
|
||||
message: event,
|
||||
source: source,
|
||||
lineno: lineno,
|
||||
colno: colno,
|
||||
error: error ? error.stack : null
|
||||
});
|
||||
//message.error("发生错误:"+event)
|
||||
return true;
|
||||
};
|
||||
|
||||
const showManagePromptsModal=ref(false)
|
||||
const promptTypeOptions=[
|
||||
{label:"模型系统Prompt",value:'模型系统Prompt'},
|
||||
{label:"模型用户Prompt",value:'模型用户Prompt'},]
|
||||
const formPromptRef=ref(null)
|
||||
const formPrompt=ref({
|
||||
ID:0,
|
||||
Name:'',
|
||||
Content:'',
|
||||
Type:'',
|
||||
})
|
||||
function managePrompts(){
|
||||
formPrompt.value.ID=0
|
||||
showManagePromptsModal.value=true
|
||||
}
|
||||
function savePrompt(){
|
||||
AddPrompt(formPrompt.value).then(res=>{
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
showManagePromptsModal.value=false
|
||||
})
|
||||
}
|
||||
function editPrompt(prompt){
|
||||
//console.log(prompt)
|
||||
formPrompt.value.ID=prompt.ID
|
||||
formPrompt.value.Name=prompt.name
|
||||
formPrompt.value.Content=prompt.content
|
||||
formPrompt.value.Type=prompt.type
|
||||
showManagePromptsModal.value=true
|
||||
}
|
||||
function deletePrompt(ID){
|
||||
DelPrompt(ID).then(res=>{
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px">
|
||||
<n-form ref="formRef" :model="formValue" :label-placement="'left'" :label-align="'left'" style="width: 100%;height: 100%">
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<n-gi :span="24">
|
||||
<n-text type="default" style="font-size: 25px;font-weight: bold">基础设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="10" label="Tushare api token:" path="tushareToken" >
|
||||
<n-flex justify="left" style="text-align: left;--wails-draggable:drag" >
|
||||
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" >
|
||||
<n-card :title="()=> h(NTag, { type: 'primary',bordered:false },()=> '基础设置')" size="small" >
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left" >
|
||||
<!-- <n-gi :span="24">-->
|
||||
<!-- <n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>-->
|
||||
<!-- </n-gi>-->
|
||||
<n-form-item-gi :span="10" label="Tushare Token:" path="tushareToken" >
|
||||
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
|
||||
<n-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="6" 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="6" label="暗黑主题:" path="darkTheme" >
|
||||
<n-switch v-model:value="formValue.darkTheme" />
|
||||
</n-form-item-gi>
|
||||
<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="default" 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="4" label="弹幕功能:" path="enableDanmu" >
|
||||
<n-switch v-model:value="formValue.enableDanmu" />
|
||||
</n-form-item-gi>
|
||||
<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="default" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="6" label="是否启用AI诊股:" path="openAI.enable" >
|
||||
<!-- <n-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="11" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
|
||||
<n-form-item-gi :span="9" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl" >
|
||||
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="请求超时时间(秒):" path="openAI.timeout">
|
||||
<n-input-number min="1" step="1" placeholder="请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="AI Timeout(秒):" title="AI请求超时时间(秒)" path="openAI.timeout" >
|
||||
<n-input-number min="60" step="1" placeholder="AI请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey):" path="openAI.apiKey">
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="Crawler Timeout(秒):" title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut" >
|
||||
<n-input-number min="30" step="1" placeholder="资讯采集超时时间(秒)" v-model:value="formValue.openAI.crawlTimeOut" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey):" path="openAI.apiKey" >
|
||||
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称:" path="openAI.model">
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称:" path="openAI.model" >
|
||||
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature:" path="openAI.temperature" >
|
||||
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="openAI maxTokens:" path="openAI.maxTokens">
|
||||
<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="22" v-if="formValue.openAI.enable" label="模型系统 Prompt:" path="openAI.prompt">
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天):" path="openAI.maxTokens" >
|
||||
<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" >
|
||||
<n-input v-model:value="formValue.openAI.prompt"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
@@ -182,19 +393,97 @@ function sendTestNotice(){
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型用户 Prompt:" path="openAI.questionTemplate" >
|
||||
<n-input v-model:value="formValue.openAI.questionTemplate"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
|
||||
:autosize="{
|
||||
minRows: 5,
|
||||
maxRows: 8
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-gi :span="24">
|
||||
<div style="display: flex; justify-content: center">
|
||||
<n-button type="primary" @click="saveConfig">
|
||||
保存
|
||||
</n-button>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-grid :cols="24">
|
||||
<n-gi :span="24">
|
||||
<n-space justify="center">
|
||||
<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-space>
|
||||
</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-flex>
|
||||
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
|
||||
<n-card
|
||||
style="width: 800px;height: 600px;text-align: left"
|
||||
:bordered="false"
|
||||
:title="(formPrompt.ID>0?'修改':'添加')+'提示词'"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'" >
|
||||
<n-form-item label="名称">
|
||||
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="类型">
|
||||
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型" />
|
||||
</n-form-item>
|
||||
<n-form-item label="内容">
|
||||
<n-input v-model:value="formPrompt.Content"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入prompt"
|
||||
:autosize="{
|
||||
minRows: 12,
|
||||
maxRows: 12,
|
||||
}"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-flex justify="end">
|
||||
<n-button type="primary" @click="savePrompt">
|
||||
保存
|
||||
</n-button>
|
||||
<n-button type="warning" @click="showManagePromptsModal=false">
|
||||
取消
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.cardHeaderClass{
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
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
@@ -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>
|
||||
@@ -5,7 +5,6 @@ import router from './router/router'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
app.mount('#app')
|
||||
@@ -1,17 +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: '/settings/:id', component: settingsView,name: 'settings' },
|
||||
{ path: '/about', component: about,name: 'about' },
|
||||
{ path: '/', component: stockView,name: 'stock'},
|
||||
{ path: '/fund', component: fundView,name: 'fund' },
|
||||
{ path: '/settings', component: settingsView,name: 'settings' },
|
||||
{ path: '/about', component: aboutView,name: 'about' },
|
||||
{ path: '/market', component: marketView,name: 'market' },
|
||||
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
//history: createWebHistory(),
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
/*background-color: rgba(27, 38, 54, 1);*/
|
||||
text-align: center;
|
||||
color: white;
|
||||
/*color: white;*/
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
/*color: white;*/
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
|
||||
106
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file → Executable file
@@ -1,25 +1,111 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {models} from '../models';
|
||||
import {data} from '../models';
|
||||
import {models} from '../models';
|
||||
|
||||
export function AddCronTask(arg1:data.FollowedStock):Promise<any>;
|
||||
|
||||
export function AddGroup(arg1:data.Group):Promise<string>;
|
||||
|
||||
export function AddPrompt(arg1:models.Prompt):Promise<string>;
|
||||
|
||||
export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
|
||||
|
||||
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
|
||||
|
||||
export function CheckSponsorCode(arg1:string):Promise<Record<string, any>>;
|
||||
|
||||
export function CheckUpdate():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>;
|
||||
|
||||
export function FollowFund(arg1:string):Promise<string>;
|
||||
|
||||
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
|
||||
|
||||
export function GetConfig():Promise<data.Settings>;
|
||||
|
||||
export function GetFollowList():Promise<Array<data.FollowedStock>>;
|
||||
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):Promise<void>;
|
||||
export function HotEvent(arg1:number):Promise<any>;
|
||||
|
||||
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||
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>;
|
||||
|
||||
@@ -29,8 +115,20 @@ export function SetAlarmChangePercent(arg1:number,arg2:number,arg3:string):Promi
|
||||
|
||||
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
|
||||
|
||||
export function SetStockAICron(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
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>;
|
||||
|
||||
export function UpdateConfig(arg1:data.Settings):Promise<string>;
|
||||
|
||||
208
frontend/wailsjs/go/main/App.js
Normal file → Executable file
@@ -2,10 +2,58 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function AddCronTask(arg1) {
|
||||
return window['go']['main']['App']['AddCronTask'](arg1);
|
||||
}
|
||||
|
||||
export function AddGroup(arg1) {
|
||||
return window['go']['main']['App']['AddGroup'](arg1);
|
||||
}
|
||||
|
||||
export function AddPrompt(arg1) {
|
||||
return window['go']['main']['App']['AddPrompt'](arg1);
|
||||
}
|
||||
|
||||
export function AddStockGroup(arg1, arg2) {
|
||||
return window['go']['main']['App']['AddStockGroup'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function AnalyzeSentiment(arg1) {
|
||||
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
|
||||
}
|
||||
|
||||
export function CheckSponsorCode(arg1) {
|
||||
return window['go']['main']['App']['CheckSponsorCode'](arg1);
|
||||
}
|
||||
|
||||
export function CheckUpdate() {
|
||||
return window['go']['main']['App']['CheckUpdate']();
|
||||
}
|
||||
|
||||
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']();
|
||||
}
|
||||
|
||||
export function Follow(arg1) {
|
||||
return window['go']['main']['App']['Follow'](arg1);
|
||||
}
|
||||
|
||||
export function FollowFund(arg1) {
|
||||
return window['go']['main']['App']['FollowFund'](arg1);
|
||||
}
|
||||
|
||||
export function GetAIResponseResult(arg1) {
|
||||
return window['go']['main']['App']['GetAIResponseResult'](arg1);
|
||||
}
|
||||
@@ -14,28 +62,152 @@ 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']();
|
||||
}
|
||||
|
||||
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) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2);
|
||||
export function HotEvent(arg1) {
|
||||
return window['go']['main']['App']['HotEvent'](arg1);
|
||||
}
|
||||
|
||||
export function SaveAIResponseResult(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3);
|
||||
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) {
|
||||
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -54,14 +226,38 @@ export function SetCostPriceAndVolume(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SetStockAICron(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetStockAICron'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SetStockSort(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetStockSort'](arg1, arg2);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function UnFollowFund(arg1) {
|
||||
return window['go']['main']['App']['UnFollowFund'](arg1);
|
||||
}
|
||||
|
||||
export function UpdateConfig(arg1) {
|
||||
return window['go']['main']['App']['UpdateConfig'](arg1);
|
||||
}
|
||||
|
||||
371
frontend/wailsjs/go/models.ts
Normal file → Executable file
@@ -1,38 +1,70 @@
|
||||
export namespace data {
|
||||
|
||||
export class FollowedStock {
|
||||
StockCode: string;
|
||||
Name: string;
|
||||
Volume: number;
|
||||
CostPrice: number;
|
||||
Price: number;
|
||||
PriceChange: number;
|
||||
ChangePercent: number;
|
||||
AlarmChangePercent: number;
|
||||
AlarmPrice: number;
|
||||
export class FundBasic {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
Time: any;
|
||||
Sort: number;
|
||||
IsDel: number;
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
code: string;
|
||||
name: string;
|
||||
fullName: string;
|
||||
type: string;
|
||||
establishment: string;
|
||||
scale: string;
|
||||
company: string;
|
||||
manager: string;
|
||||
rating: string;
|
||||
trackingTarget: string;
|
||||
netUnitValue?: number;
|
||||
netUnitValueDate: string;
|
||||
netEstimatedUnit?: number;
|
||||
netEstimatedUnitTime: string;
|
||||
netAccumulated?: number;
|
||||
netGrowth1?: number;
|
||||
netGrowth3?: number;
|
||||
netGrowth6?: number;
|
||||
netGrowth12?: number;
|
||||
netGrowth36?: number;
|
||||
netGrowth60?: number;
|
||||
netGrowthYTD?: number;
|
||||
netGrowthAll?: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedStock(source);
|
||||
return new FundBasic(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.StockCode = source["StockCode"];
|
||||
this.Name = source["Name"];
|
||||
this.Volume = source["Volume"];
|
||||
this.CostPrice = source["CostPrice"];
|
||||
this.Price = source["Price"];
|
||||
this.PriceChange = source["PriceChange"];
|
||||
this.ChangePercent = source["ChangePercent"];
|
||||
this.AlarmChangePercent = source["AlarmChangePercent"];
|
||||
this.AlarmPrice = source["AlarmPrice"];
|
||||
this.Time = this.convertValues(source["Time"], null);
|
||||
this.Sort = source["Sort"];
|
||||
this.IsDel = source["IsDel"];
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.code = source["code"];
|
||||
this.name = source["name"];
|
||||
this.fullName = source["fullName"];
|
||||
this.type = source["type"];
|
||||
this.establishment = source["establishment"];
|
||||
this.scale = source["scale"];
|
||||
this.company = source["company"];
|
||||
this.manager = source["manager"];
|
||||
this.rating = source["rating"];
|
||||
this.trackingTarget = source["trackingTarget"];
|
||||
this.netUnitValue = source["netUnitValue"];
|
||||
this.netUnitValueDate = source["netUnitValueDate"];
|
||||
this.netEstimatedUnit = source["netEstimatedUnit"];
|
||||
this.netEstimatedUnitTime = source["netEstimatedUnitTime"];
|
||||
this.netAccumulated = source["netAccumulated"];
|
||||
this.netGrowth1 = source["netGrowth1"];
|
||||
this.netGrowth3 = source["netGrowth3"];
|
||||
this.netGrowth6 = source["netGrowth6"];
|
||||
this.netGrowth12 = source["netGrowth12"];
|
||||
this.netGrowth36 = source["netGrowth36"];
|
||||
this.netGrowth60 = source["netGrowth60"];
|
||||
this.netGrowthYTD = source["netGrowthYTD"];
|
||||
this.netGrowthAll = source["netGrowthAll"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -53,6 +85,231 @@ export namespace data {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class FollowedFund {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
code: string;
|
||||
name: string;
|
||||
netUnitValue?: number;
|
||||
netUnitValueDate: string;
|
||||
netEstimatedUnit?: number;
|
||||
netEstimatedUnitTime: string;
|
||||
netAccumulated?: number;
|
||||
netEstimatedRate?: number;
|
||||
fundBasic: FundBasic;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedFund(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.code = source["code"];
|
||||
this.name = source["name"];
|
||||
this.netUnitValue = source["netUnitValue"];
|
||||
this.netUnitValueDate = source["netUnitValueDate"];
|
||||
this.netEstimatedUnit = source["netEstimatedUnit"];
|
||||
this.netEstimatedUnitTime = source["netEstimatedUnitTime"];
|
||||
this.netAccumulated = source["netAccumulated"];
|
||||
this.netEstimatedRate = source["netEstimatedRate"];
|
||||
this.fundBasic = this.convertValues(source["fundBasic"], FundBasic);
|
||||
}
|
||||
|
||||
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 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;
|
||||
Volume: number;
|
||||
CostPrice: number;
|
||||
Price: number;
|
||||
PriceChange: number;
|
||||
ChangePercent: number;
|
||||
AlarmChangePercent: number;
|
||||
AlarmPrice: number;
|
||||
// Go type: time
|
||||
Time: any;
|
||||
Sort: number;
|
||||
Cron?: string;
|
||||
IsDel: number;
|
||||
Groups: GroupStock[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedStock(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.StockCode = source["StockCode"];
|
||||
this.Name = source["Name"];
|
||||
this.Volume = source["Volume"];
|
||||
this.CostPrice = source["CostPrice"];
|
||||
this.Price = source["Price"];
|
||||
this.PriceChange = source["PriceChange"];
|
||||
this.ChangePercent = source["ChangePercent"];
|
||||
this.AlarmChangePercent = source["AlarmChangePercent"];
|
||||
this.AlarmPrice = source["AlarmPrice"];
|
||||
this.Time = this.convertValues(source["Time"], null);
|
||||
this.Sort = source["Sort"];
|
||||
this.Cron = source["Cron"];
|
||||
this.IsDel = source["IsDel"];
|
||||
this.Groups = this.convertValues(source["Groups"], GroupStock);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class 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
|
||||
@@ -76,6 +333,17 @@ export namespace data {
|
||||
openAiApiTimeOut: number;
|
||||
prompt: string;
|
||||
checkUpdate: boolean;
|
||||
questionTemplate: string;
|
||||
crawlTimeOut: number;
|
||||
kDays: number;
|
||||
enableDanmu: boolean;
|
||||
browserPath: string;
|
||||
enableNews: boolean;
|
||||
darkTheme: boolean;
|
||||
browserPoolSize: number;
|
||||
enableFund: boolean;
|
||||
enablePushNews: boolean;
|
||||
sponsorCode: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Settings(source);
|
||||
@@ -102,6 +370,17 @@ export namespace data {
|
||||
this.openAiApiTimeOut = source["openAiApiTimeOut"];
|
||||
this.prompt = source["prompt"];
|
||||
this.checkUpdate = source["checkUpdate"];
|
||||
this.questionTemplate = source["questionTemplate"];
|
||||
this.crawlTimeOut = source["crawlTimeOut"];
|
||||
this.kDays = source["kDays"];
|
||||
this.enableDanmu = source["enableDanmu"];
|
||||
this.browserPath = source["browserPath"];
|
||||
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 {
|
||||
@@ -237,6 +516,9 @@ export namespace data {
|
||||
"卖四申报": string;
|
||||
"卖五报价": string;
|
||||
"卖五申报": string;
|
||||
"市场": string;
|
||||
"盘前盘后": string;
|
||||
"盘前盘后涨跌幅": string;
|
||||
changePercent: number;
|
||||
changePrice: number;
|
||||
highRate: number;
|
||||
@@ -249,6 +531,7 @@ export namespace data {
|
||||
sort: number;
|
||||
alarmChangePercent: number;
|
||||
alarmPrice: number;
|
||||
Groups: GroupStock[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StockInfo(source);
|
||||
@@ -294,6 +577,9 @@ export namespace data {
|
||||
this["卖四申报"] = source["卖四申报"];
|
||||
this["卖五报价"] = source["卖五报价"];
|
||||
this["卖五申报"] = source["卖五申报"];
|
||||
this["市场"] = source["市场"];
|
||||
this["盘前盘后"] = source["盘前盘后"];
|
||||
this["盘前盘后涨跌幅"] = source["盘前盘后涨跌幅"];
|
||||
this.changePercent = source["changePercent"];
|
||||
this.changePrice = source["changePrice"];
|
||||
this.highRate = source["highRate"];
|
||||
@@ -306,6 +592,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 {
|
||||
@@ -339,8 +626,11 @@ export namespace models {
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
chatId: string;
|
||||
modelName: string;
|
||||
stockCode: string;
|
||||
stockName: string;
|
||||
question: string;
|
||||
content: string;
|
||||
IsDel: number;
|
||||
|
||||
@@ -354,8 +644,11 @@ export namespace models {
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.chatId = source["chatId"];
|
||||
this.modelName = source["modelName"];
|
||||
this.stockCode = source["stockCode"];
|
||||
this.stockName = source["stockName"];
|
||||
this.question = source["question"];
|
||||
this.content = source["content"];
|
||||
this.IsDel = source["IsDel"];
|
||||
}
|
||||
@@ -378,6 +671,24 @@ export namespace models {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class Prompt {
|
||||
ID: number;
|
||||
name: string;
|
||||
content: string;
|
||||
type: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Prompt(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.name = source["name"];
|
||||
this.content = source["content"];
|
||||
this.type = source["type"];
|
||||
}
|
||||
}
|
||||
export class VersionInfo {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
@@ -389,7 +700,11 @@ export namespace models {
|
||||
version: string;
|
||||
content: string;
|
||||
icon: string;
|
||||
alipay: string;
|
||||
wxpay: string;
|
||||
wxgzh: string;
|
||||
buildTimeStamp: number;
|
||||
officialStatement: string;
|
||||
IsDel: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
@@ -405,7 +720,11 @@ export namespace models {
|
||||
this.version = source["version"];
|
||||
this.content = source["content"];
|
||||
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"];
|
||||
}
|
||||
|
||||
|
||||
2
frontend/wailsjs/runtime/runtime.d.ts
vendored
@@ -134,7 +134,7 @@ export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): Promise<Size>;
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
|
||||
72
go.mod
@@ -1,21 +1,28 @@
|
||||
module go-stock
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.0
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/coocood/freecache v1.2.4
|
||||
github.com/duke-git/lancet/v2 v2.3.4
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/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/wailsapp/wails/v2 v2.9.2
|
||||
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/text v0.21.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
|
||||
@@ -23,56 +30,61 @@ 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
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect
|
||||
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/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||
github.com/esiqveland/notify v0.13.3 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/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
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.10.2 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
|
||||
github.com/leaanthony/gosod v1.0.3 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/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/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // 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.4 // indirect
|
||||
github.com/samber/lo v1.38.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.6 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // 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/wailsapp/go-webview2 v1.0.16 // 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.31.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.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
|
||||
165
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=
|
||||
@@ -21,32 +23,24 @@ github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvY
|
||||
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
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=
|
||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
@@ -55,13 +49,18 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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=
|
||||
@@ -72,47 +71,47 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
|
||||
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
|
||||
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
|
||||
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
|
||||
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/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=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -121,29 +120,52 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/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/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
|
||||
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
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.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
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/wailsapp/go-webview2 v1.0.16 h1:wffnvnkkLvhRex/aOrA3R7FP7rkvOqL/bir1br7BekU=
|
||||
github.com/wailsapp/go-webview2 v1.0.16/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
|
||||
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=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
|
||||
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
|
||||
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
|
||||
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
@@ -156,8 +178,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/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=
|
||||
@@ -174,8 +197,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/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=
|
||||
@@ -184,16 +208,11 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -204,8 +223,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/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=
|
||||
@@ -224,10 +244,11 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/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=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
@@ -235,12 +256,14 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
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.0-20210107192922-496545a6307b/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=
|
||||
|
||||
297
main.go
@@ -1,9 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"fmt"
|
||||
"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"
|
||||
@@ -14,11 +16,12 @@ import (
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
log "go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"log"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"time"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
@@ -30,82 +33,110 @@ var icon []byte
|
||||
//go:embed build/app.ico
|
||||
var icon2 []byte
|
||||
|
||||
//go:embed build/screenshot/alipay.jpg
|
||||
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
|
||||
|
||||
//go:embed build/stock_base_info_hk.json
|
||||
var stocksBinHK []byte
|
||||
|
||||
//go:embed build/stock_base_info_us.json
|
||||
var stocksBinUS []byte
|
||||
|
||||
//go:generate cp -R ./data ./build/bin
|
||||
|
||||
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{})
|
||||
go AutoMigrate()
|
||||
|
||||
if stocksBin != nil && len(stocksBin) > 0 {
|
||||
go initStockData()
|
||||
}
|
||||
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()
|
||||
AppMenu.Append(menu.EditMenu())
|
||||
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)
|
||||
})
|
||||
//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("h"), func(_ *menu.CallbackData) {
|
||||
FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("z"), func(_ *menu.CallbackData) {
|
||||
runtime.WindowHide(app.ctx)
|
||||
})
|
||||
|
||||
FileMenu.AddText("显示", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {
|
||||
runtime.WindowShow(app.ctx)
|
||||
})
|
||||
}
|
||||
|
||||
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
|
||||
// runtime.Quit(app.ctx)
|
||||
//})
|
||||
logger.NewDefaultLogger().Info("version: " + Version)
|
||||
logger.NewDefaultLogger().Info("commit: " + VersionCommit)
|
||||
log.SugaredLogger.Info("version: " + Version)
|
||||
log.SugaredLogger.Info("commit: " + VersionCommit)
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "go-stock",
|
||||
Width: 1366,
|
||||
Height: 920,
|
||||
MinWidth: 1024,
|
||||
MinHeight: 768,
|
||||
MaxWidth: 1920,
|
||||
MaxHeight: 960,
|
||||
//var width, height int
|
||||
//var err error
|
||||
//
|
||||
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}
|
||||
if darkTheme {
|
||||
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: 900,
|
||||
MinWidth: minWidth,
|
||||
MinHeight: minHeight,
|
||||
//MaxWidth: width,
|
||||
//MaxHeight: height,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: true,
|
||||
Frameless: frameless,
|
||||
StartHidden: false,
|
||||
HideWindowOnClose: false,
|
||||
EnableDefaultContextMenu: true,
|
||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
BackgroundColour: backgroundColour,
|
||||
Assets: assets,
|
||||
Menu: AppMenu,
|
||||
Logger: nil,
|
||||
@@ -116,6 +147,10 @@ func main() {
|
||||
OnBeforeClose: app.beforeClose,
|
||||
OnShutdown: app.shutdown,
|
||||
WindowStartState: options.Normal,
|
||||
SingleInstanceLock: &options.SingleInstanceLock{
|
||||
UniqueId: "go-stock",
|
||||
OnSecondInstanceLaunch: OnSecondInstanceLaunch,
|
||||
},
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
@@ -130,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,
|
||||
@@ -149,7 +183,80 @@ func main() {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.SugaredLogger.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
log.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
log.SugaredLogger.Infof("init stock data us %d", len(v))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
log.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
log.SugaredLogger.Infof("init stock data hk %d", len(v))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -163,46 +270,92 @@ func updateBasicInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
func initStockData() {
|
||||
var count int64
|
||||
db.Dao.Model(&data.StockBasic{}).Count(&count)
|
||||
if count > 0 {
|
||||
return
|
||||
}
|
||||
logger.NewDefaultLogger().Info("init stock data")
|
||||
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)
|
||||
if err != nil {
|
||||
logger.NewDefaultLogger().Error(err.Error())
|
||||
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])
|
||||
db.Dao.Model(&data.StockBasic{}).FirstOrCreate(stock, &data.StockBasic{TsCode: stock.TsCode}).Updates(stock)
|
||||
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 {
|
||||
continue
|
||||
} 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) {
|
||||
_, err := os.Stat(dir)
|
||||
if os.IsNotExist(err) {
|
||||
os.Mkdir(dir, os.ModePerm)
|
||||
logger.NewDefaultLogger().Info("create dir: " + dir)
|
||||
log.SugaredLogger.Info("create dir: " + dir)
|
||||
}
|
||||
if BuildKey == "" {
|
||||
BuildKey = "cc1e0d684e32f176c56ff1fcf384dcd9"
|
||||
}
|
||||
}
|
||||
|
||||
// PanicHandler 捕获 panic 的包装函数
|
||||
func PanicHandler() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Printf("Recovered from panic: %v\n", r)
|
||||
debug.PrintStack()
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||