Compare commits

...

231 Commits

Author SHA1 Message Date
zdl
04248e5a99 feat: 修复logger 报错 2025-11-25 15:30:43 +08:00
zdl
af54d8e070 feat: 修复debugger报错 2025-11-25 15:30:43 +08:00
zdl
a3cb5e928e feat: 拆分LeftSidebar 组件为ts组件 2025-11-25 15:30:43 +08:00
b9eddbe752 update pay function 2025-11-25 15:28:12 +08:00
zdl
cb9f927e3e feat: 调整逻辑如果用户未登录且不在首页,跳转到首页 2025-11-25 14:28:20 +08:00
zdl
b9a587bac4 feat: 去掉logger 2025-11-25 14:28:20 +08:00
zdl
86259793cb feat: bug修复 2025-11-25 14:28:19 +08:00
f76bd17160 update pay function 2025-11-25 11:22:34 +08:00
ce0e91a5fb update pay function 2025-11-25 10:16:04 +08:00
f873fdb9a6 update pay function 2025-11-25 10:09:47 +08:00
cc446fc0da update pay function 2025-11-25 09:50:12 +08:00
de30755271 update pay function 2025-11-25 08:08:01 +08:00
a2f33c2a8a update pay function 2025-11-25 08:00:56 +08:00
761fe5d2f0 update pay function 2025-11-25 07:50:33 +08:00
3677217fce update pay function 2025-11-25 07:35:15 +08:00
177c1d6401 update pay function 2025-11-24 23:45:58 +08:00
fb066aa6b8 update pay function 2025-11-24 23:18:12 +08:00
96bedb8439 update pay function 2025-11-24 21:23:09 +08:00
83d7c19fed update pay function 2025-11-24 20:15:19 +08:00
e80d2cfcec update pay function 2025-11-24 20:06:51 +08:00
412f2a3d79 update pay function 2025-11-24 19:49:42 +08:00
4a0e156bec update pay function 2025-11-24 19:28:52 +08:00
7743a8a26a update pay function 2025-11-24 19:22:22 +08:00
72e3e56a63 update pay function 2025-11-24 19:07:24 +08:00
388e9eb235 Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref 2025-11-24 16:58:08 +08:00
bd23100192 update pay function 2025-11-24 16:58:02 +08:00
zdl
887525197a feat: StockChartAntdModal UI调整 2025-11-24 16:53:37 +08:00
zdl
f8bb46ae64 feat: 添加mock 2025-11-24 16:53:37 +08:00
810c878a1e update pay function 2025-11-24 16:49:04 +08:00
2607028f4f Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref 2025-11-24 16:39:47 +08:00
ea166d59c4 update pay function 2025-11-24 16:39:36 +08:00
zdl
982d8135e7 feat: bug修复 2025-11-24 16:38:33 +08:00
zdl
e61090810b Merge branch 'feature_2025/251117_pref' into feature_2025/251121_h5UI
* feature_2025/251117_pref: (159 commits)
  feat: UI调整
  feat: 将滚动事件移东到组件内部
  feat: 去掉背景组件
  feat: 拆分左侧栏、中间聊天区、右侧栏组件, Hooks 提取
  feat: 简化主组件 index.js - 使用组件组合方式重构
  feat: 创建 ChatArea 组件(含 MessageRenderer、ExecutionStepsDisplay 子组件)
  feat:拆分工具函数
  feat: 拆分BackgroundEffects 背景渐变装饰层
  feat: RightSidebar (~420 行) - 模型/工具/统计 Tab 面板(单文件)
  feat:  LeftSidebar (~280 行) - 对话历史列表 + 用户信息卡片
  feat: 修复bug
  pref:移除黑夜模式
  feat: 修复警告
  feat: 提取常量配置
  feat: 修复ts报错
  feat:  StockChartModal.tsx 替换 KLine 实现
  update pay function
  update pay function
  update pay function
  update pay function
  ...
2025-11-24 16:32:24 +08:00
zdl
2d49af3bea feat: UI调整 2025-11-24 16:11:13 +08:00
zdl
3a0898634f feat: 将滚动事件移东到组件内部 2025-11-24 15:54:26 +08:00
zdl
44ecf7e5c7 feat: 去掉背景组件 2025-11-24 15:47:23 +08:00
zdl
5183473557 feat: 拆分左侧栏、中间聊天区、右侧栏组件, Hooks 提取 2025-11-24 15:23:22 +08:00
zdl
41f1bbab1b feat: 简化主组件 index.js - 使用组件组合方式重构 2025-11-24 15:18:52 +08:00
zdl
f536d68753 feat: 创建 ChatArea 组件(含 MessageRenderer、ExecutionStepsDisplay 子组件) 2025-11-24 15:18:32 +08:00
zdl
9475027c0d feat:拆分工具函数 2025-11-24 15:12:52 +08:00
zdl
851c148f7d feat: 拆分BackgroundEffects 背景渐变装饰层 2025-11-24 15:12:24 +08:00
zdl
ef7f91ba77 feat: RightSidebar (~420 行) - 模型/工具/统计 Tab 面板(单文件) 2025-11-24 15:11:52 +08:00
zdl
80084d607b feat: LeftSidebar (~280 行) - 对话历史列表 + 用户信息卡片 2025-11-24 15:11:19 +08:00
zdl
dc789f57f7 feat: 修复bug 2025-11-24 15:10:08 +08:00
zdl
528e61b961 pref:移除黑夜模式 2025-11-24 15:07:13 +08:00
zdl
e201f35b18 feat: 修复警告 2025-11-24 14:52:57 +08:00
zdl
13040b5df8 feat: 提取常量配置 2025-11-24 14:31:50 +08:00
zdl
9b068fd69f feat: 修复ts报错 2025-11-24 14:08:41 +08:00
zdl
2f125a9207 feat: StockChartModal.tsx 替换 KLine 实现 2025-11-24 13:59:44 +08:00
b4dcbd1db9 update pay function 2025-11-24 08:05:19 +08:00
c594650aa4 update pay function 2025-11-24 07:21:02 +08:00
8c372bbc89 update pay function 2025-11-23 23:44:36 +08:00
4054e2e106 update pay function 2025-11-23 23:32:35 +08:00
0a149eaa0f update pay function 2025-11-23 23:17:12 +08:00
3c7b55226c update pay function 2025-11-23 23:08:30 +08:00
69d05b664e update pay function 2025-11-23 23:00:25 +08:00
ce2226793f update pay function 2025-11-23 22:48:27 +08:00
07a4cdb357 update pay function 2025-11-23 22:41:16 +08:00
d9a169d2e0 update pay function 2025-11-23 22:27:57 +08:00
76bf560b36 update pay function 2025-11-23 22:23:19 +08:00
4a411c6d44 update pay function 2025-11-23 22:11:03 +08:00
dca70074c0 update pay function 2025-11-23 22:06:07 +08:00
1f1aa896d1 update pay function 2025-11-23 21:42:48 +08:00
134897c3aa update pay function 2025-11-23 21:09:31 +08:00
19db421f9f update pay function 2025-11-23 21:04:34 +08:00
1c290e0da2 update pay function 2025-11-23 20:42:58 +08:00
15def1c931 update pay function 2025-11-23 20:34:49 +08:00
7538f2d935 update pay function 2025-11-23 20:12:54 +08:00
3fa3e52d65 update pay function 2025-11-23 19:44:49 +08:00
2fb12e0cc7 update pay function 2025-11-23 18:48:12 +08:00
13f8e2a4f1 update pay function 2025-11-23 18:24:07 +08:00
7b3907a3bd update pay function 2025-11-23 18:11:48 +08:00
b582de9bc2 update pay function 2025-11-23 17:19:56 +08:00
acb7862789 update pay function 2025-11-23 16:57:02 +08:00
a778f94b68 update pay function 2025-11-23 16:34:45 +08:00
23a94d5ab2 update pay function 2025-11-23 16:23:18 +08:00
d5250f7d3c update pay function 2025-11-23 15:58:14 +08:00
ae92f333c4 update pay function 2025-11-23 15:40:58 +08:00
82146f7365 Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref 2025-11-23 14:42:05 +08:00
96346977ae update pay function 2025-11-23 14:38:29 +08:00
zdl
0f410c55a5 feat: StockChartModal.tsx 2025-11-23 14:35:24 +08:00
zdl
a4b8a13e6d feat: 抽离关联描述组件 2025-11-23 14:35:24 +08:00
f578969ee6 update pay function 2025-11-23 14:25:38 +08:00
4da1d580fc update pay function 2025-11-23 13:53:13 +08:00
af362f3ceb update pay function 2025-11-23 13:40:46 +08:00
e01092365e update pay function 2025-11-23 13:15:41 +08:00
ad7c180e11 update pay function 2025-11-23 12:54:51 +08:00
2111b1d25b update pay function 2025-11-23 12:45:44 +08:00
ddcbbc9da4 update pay function 2025-11-23 12:35:48 +08:00
6515a47a42 update pay function 2025-11-23 12:31:34 +08:00
0bcf6a93f7 update pay function 2025-11-23 12:21:19 +08:00
5857144180 update pay function 2025-11-23 12:11:12 +08:00
1ea001fa3d update pay function 2025-11-23 11:14:48 +08:00
09420963d5 update pay function 2025-11-23 11:04:27 +08:00
d8a1dd7a03 update pay function 2025-11-23 10:49:07 +08:00
zdl
098107f38e feat: 调整关联描述UI 2025-11-23 10:21:04 +08:00
zdl
c2b80a727d fix: 删除调试信息 2025-11-23 10:09:01 +08:00
zdl
745b9caeee feat: StockListItem.js - 分时/K线点击切换效果修复 2025-11-23 10:02:13 +08:00
b1d042d0e3 update pay function 2025-11-23 09:19:32 +08:00
04c13f3a6c update pay function 2025-11-23 09:01:00 +08:00
173ddb985d update pay function 2025-11-23 08:42:08 +08:00
c487c33617 update pay function 2025-11-23 08:35:29 +08:00
9251531eb7 update pay function 2025-11-23 08:30:52 +08:00
738cc9cb87 update pay function 2025-11-23 08:24:30 +08:00
7b9bb153cc update pay function 2025-11-23 08:17:23 +08:00
33ae9e63a1 update pay function 2025-11-23 08:06:43 +08:00
c4efebdbda update pay function 2025-11-23 08:02:40 +08:00
602888bbeb update pay function 2025-11-23 07:55:32 +08:00
6a1e861977 update pay function 2025-11-23 07:41:21 +08:00
31a3e429d7 update pay function 2025-11-23 07:15:07 +08:00
bbc2493ecd update pay function 2025-11-23 07:08:07 +08:00
eef1dbfe8d update pay function 2025-11-23 06:36:39 +08:00
aaef2272f1 update pay function 2025-11-23 00:24:05 +08:00
9f2fd60228 update pay function 2025-11-23 00:21:31 +08:00
2fc0cca482 update pay function 2025-11-23 00:12:24 +08:00
2668affe88 update pay function 2025-11-23 00:03:52 +08:00
32b4b772c5 update pay function 2025-11-22 23:56:17 +08:00
115300a4e3 update pay function 2025-11-22 23:51:03 +08:00
zdl
2964b4331a feat: 处理报错 2025-11-22 23:39:45 +08:00
cbc231a2b6 Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref 2025-11-22 23:29:38 +08:00
a158319717 update pay function 2025-11-22 23:29:25 +08:00
zdl
f361cb55f4 feat: 现在创建主组件: 2025-11-22 23:29:08 +08:00
zdl
bcd67ed410 feat: 创建自定义 Hooks 2025-11-22 23:29:08 +08:00
zdl
c391c4c980 feat: 现在创建配置文件。首先让我检查现有的主题配置,以保持样式一致性: 2025-11-22 23:29:08 +08:00
zdl
7b2f5a18bc feat:StockChart 类型定义统一导出 2025-11-22 23:29:08 +08:00
zdl
06916cdde5 feat:股票相关类型定义 2025-11-22 23:29:08 +08:00
zdl
5bb8a17588 feat: 创建类型定义文件 2025-11-22 23:29:08 +08:00
zdl
ad2a374069 feat: 完全替换现有的 ECharts 股票图表弹窗,使用专业的 KLineChart 库 + TypeScript,提升性能和可维护性。
安装依赖
2025-11-22 23:29:08 +08:00
f28bba6326 update pay function 2025-11-22 23:19:38 +08:00
69a2c83bd0 update pay function 2025-11-22 23:01:34 +08:00
c5f21a517d update pay function 2025-11-22 21:54:04 +08:00
6b9be7dad0 update pay function 2025-11-22 21:26:55 +08:00
3526c8c51c update pay function 2025-11-22 20:42:25 +08:00
13609163a7 update pay function 2025-11-22 20:36:45 +08:00
e4961a21ee update pay function 2025-11-22 20:31:45 +08:00
4fcc3e1054 update pay function 2025-11-22 20:19:38 +08:00
b2c116cef4 update pay function 2025-11-22 20:13:40 +08:00
1ad68bca6c update pay function 2025-11-22 20:11:10 +08:00
4879121d2b update pay function 2025-11-22 19:42:32 +08:00
cde849b3a4 update pay function 2025-11-22 18:58:14 +08:00
6c99cb83bf update pay function 2025-11-22 18:10:55 +08:00
97fd1645d4 update pay function 2025-11-22 17:51:48 +08:00
a66d55237f update pay function 2025-11-22 17:41:54 +08:00
1f7308a512 update pay function 2025-11-22 17:15:09 +08:00
cab5cc5d7b update pay function 2025-11-22 17:09:10 +08:00
47e2380bd3 update pay function 2025-11-22 16:48:23 +08:00
357c03aee2 update pay function 2025-11-22 16:41:22 +08:00
75e7e7e19c update pay function 2025-11-22 16:27:33 +08:00
f56df0e956 update pay function 2025-11-22 16:14:49 +08:00
75696b9e52 update pay function 2025-11-22 16:12:09 +08:00
5e333ad7e7 update pay function 2025-11-22 16:05:21 +08:00
70376b3544 update pay function 2025-11-22 16:03:53 +08:00
a15830c97e update pay function 2025-11-22 15:57:17 +08:00
a8d38e85d2 update pay function 2025-11-22 15:51:39 +08:00
dce6b5701f update pay function 2025-11-22 15:50:06 +08:00
0fcb7322ed update pay function 2025-11-22 15:48:31 +08:00
8e16d3cd3a update pay function 2025-11-22 15:43:21 +08:00
9b436523ff update pay function 2025-11-22 15:40:21 +08:00
59a5a03637 update pay function 2025-11-22 15:34:18 +08:00
70af97e9ad update pay function 2025-11-22 15:30:59 +08:00
ebf7ddda6a update pay function 2025-11-22 15:10:23 +08:00
68fa1d0717 update pay function 2025-11-22 13:52:23 +08:00
8fb6992cf6 update pay function 2025-11-22 13:23:50 +08:00
8f3e2bed70 update pay function 2025-11-22 13:09:46 +08:00
8a87cd1b74 update pay function 2025-11-22 12:12:45 +08:00
244968a1cb update pay function 2025-11-22 12:07:55 +08:00
47be4584f9 update pay function 2025-11-22 11:49:20 +08:00
42b7d2ee63 update pay function 2025-11-22 11:42:43 +08:00
d8e4c737c5 update pay function 2025-11-22 11:41:56 +08:00
a4b634abff update pay function 2025-11-22 10:42:30 +08:00
15d521dd59 update pay function 2025-11-22 10:37:15 +08:00
40b57c1a81 update pay function 2025-11-22 10:28:37 +08:00
71f3834b79 update pay function 2025-11-22 10:11:36 +08:00
20c6356842 update pay function 2025-11-22 10:01:04 +08:00
cd926bb42d update pay function 2025-11-22 09:57:30 +08:00
feb08dc746 update pay function 2025-11-22 09:51:17 +08:00
cddf82ce51 update pay function 2025-11-22 09:36:58 +08:00
eceb2e7da0 update pay function 2025-11-22 09:27:12 +08:00
092c86f3d2 update pay function 2025-11-22 09:16:12 +08:00
7498e87d31 update pay function 2025-11-22 09:10:13 +08:00
e778742590 update pay function 2025-11-22 08:57:37 +08:00
990ca3663e update pay function 2025-11-22 07:24:55 +08:00
b9ed0f5449 update pay function 2025-11-22 07:15:03 +08:00
077f8d9120 update pay function 2025-11-22 06:54:16 +08:00
97371ae16a update pay function 2025-11-22 00:26:07 +08:00
aa3fe0d806 update pay function 2025-11-22 00:17:25 +08:00
e68acfe7d1 update pay function 2025-11-22 00:06:44 +08:00
c336be5cd7 update pay function 2025-11-21 23:59:31 +08:00
1a845f54e9 update pay function 2025-11-21 23:53:39 +08:00
781710ae53 update pay function 2025-11-21 23:50:19 +08:00
b5a0b7094a update pay function 2025-11-21 23:41:33 +08:00
22bb57b52f update pay function 2025-11-21 23:18:19 +08:00
cd315a718f update pay function 2025-11-21 23:12:52 +08:00
ff2ad14246 update pay function 2025-11-21 23:05:29 +08:00
zdl
baf4ca1ed4 feat: 屏蔽 STOMP WebSocket 错误日志(不影响功能) 2025-11-21 18:45:13 +08:00
zdl
3cd34d93c8 Merge branch 'feature_2025/251117_pref' into feature_2025/251121_h5UI
* feature_2025/251117_pref:
  update pay function
  update pay function
  update pay function
  update pay function
  update pay function
  update pay function
  update pay function
2025-11-21 18:30:51 +08:00
zdl
c9084ebb33 feat: Socket.IO 连接地址(Mock 模式下连接生产环境) 2025-11-21 18:22:18 +08:00
zdl
ed584b72d4 feat: 创建整合所有指标的 Hook 2025-11-21 18:14:29 +08:00
zdl
2dec587d37 feat: 扩展 PostHog 事件常量 2025-11-21 18:13:53 +08:00
zdl
7f021dcfa0 feat; 创建首屏指标收集 Hook 2025-11-21 18:13:33 +08:00
zdl
e34f5593b4 feat: 创建资源加载监控工具 2025-11-21 18:12:58 +08:00
zdl
5f76530e80 feat: 创建 Web Vitals 监控工具 2025-11-21 18:12:34 +08:00
zdl
d6c7d64e59 feat: 创建性能阈值配置 2025-11-21 18:11:26 +08:00
zdl
ceed71eca4 feat: 创建 TypeScript 类型定义 2025-11-21 18:11:03 +08:00
zdl
9669d5709e fix: 在 craco.config.js 中将 /bytedesk 代理移出 Mock 模式条件判断
现在 /bytedesk 代理始终启用,指向 https://valuefrontier.cn
2025-11-21 18:06:21 +08:00
zdl
34bae35858 fix: 修复的问题:H5 汉堡菜单位置调整(移到头像右侧)
平板端显示 MoreMenu 而非汉堡菜单
未登录时不显示汉堡菜单
2025-11-21 17:59:03 +08:00
zdl
bc50d9fe3e fix: 修复的问题: │ │
│ │ -  React 18 Portal insertBefore 错误                                                                                                                               │ │
│ │ -  Ant Design Modal defaultProps 废弃警告
2025-11-21 17:46:07 +08:00
zdl
39978c57d5 pref: src/views/LimitAnalyse 页面 "数据分析"卡片中的"热词云图" 依赖更新 2025-11-21 17:37:56 +08:00
b197d62c31 update pay function 2025-11-21 14:47:47 +08:00
zdl
834067f679 fix: 修改 GlobalComponents.js(缓存 config)登录时不会触发 BytedeskWidget 重新加载 2025-11-21 14:38:09 +08:00
564caa08c2 update pay function 2025-11-21 14:34:15 +08:00
0aa050b95f update pay function 2025-11-21 14:26:26 +08:00
e22e8339a6 update pay function 2025-11-21 14:07:18 +08:00
zdl
e8b3d13c0a feat: 桌面端导航判断调整 2025-11-21 14:07:04 +08:00
8c787a8915 update pay function 2025-11-21 13:56:43 +08:00
zdl
796c623197 fix:优化h5/菜单UI 2025-11-21 13:55:06 +08:00
690754e416 update pay function 2025-11-21 13:49:43 +08:00
12d104cc22 update pay function 2025-11-21 13:41:49 +08:00
zdl
a1c1a36f6a pref: 删除doc文件 2025-11-21 11:43:08 +08:00
2b30d10451 update pay function 2025-11-20 18:00:21 +08:00
8dfd344806 update pay function 2025-11-20 16:59:09 +08:00
7c8310eeb6 update pay function 2025-11-20 16:19:52 +08:00
30108b297c update pay function 2025-11-20 16:11:05 +08:00
161bcec55e Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref 2025-11-20 15:54:46 +08:00
34f2d7dabd update pay function 2025-11-20 15:54:40 +08:00
zdl
3e4b47dbfe Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref
* 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react:
  update pay function
  update pay function
  update pay function
2025-11-20 15:47:38 +08:00
zdl
e2861b994b feat: 修改bug引入错误 2025-11-20 15:46:43 +08:00
6b9291a4f9 update pay function 2025-11-20 15:44:57 +08:00
0818eeedf1 update pay function 2025-11-20 15:34:10 +08:00
2a8d7438c8 Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref 2025-11-20 15:25:05 +08:00
fdd58634e6 update pay function 2025-11-20 15:24:57 +08:00
209 changed files with 40645 additions and 10032 deletions

View File

@@ -29,6 +29,10 @@ NODE_OPTIONS=--max_old_space_size=4096
# MSW 会在浏览器层拦截这些请求,不需要真实的后端地址
REACT_APP_API_URL=
# Socket.IO 连接地址Mock 模式下连接生产环境)
# 注意WebSocket 不被 MSW 拦截,可以独立配置
REACT_APP_SOCKET_URL=https://valuefrontier.cn
# 启用 Mock 数据(核心配置)
# 此配置会触发 src/index.js 中的 MSW 初始化
REACT_APP_ENABLE_MOCK=true

View File

@@ -44,7 +44,10 @@
**前端**
- **核心框架**: React 18.3.1
- **类型系统**: TypeScript 5.9.3(渐进式接入中,支持 JS/TS 混合开发)
- **UI 组件库**: Chakra UI 2.10.9(主要) + Ant Design 5.27.4(表格/表单)
- **UI 组件库**:
- Chakra UI 2.10.9(主要,全局使用)
- Ant Design 5.27.4(表格/表单)
- **HeroUI 3.0.0-beta**AgentChat 专用2025-11-22 升级)
- **状态管理**: Redux Toolkit 2.9.2
- **路由**: React Router v6.30.1 配合 React.lazy() 实现代码分割
- **构建系统**: CRACO 7.1.0 + 激进的 webpack 5 优化
@@ -59,6 +62,8 @@
- **虚拟化**: @tanstack/react-virtual 3.13.12(性能优化)
- **其他**: Draft.js富文本编辑、React Markdown、React Quill
**注意**: HeroUI v3 文档参考 https://v3.heroui.com/llms.txt详细升级说明见 [HEROUI_V3_UPGRADE_GUIDE.md](./HEROUI_V3_UPGRADE_GUIDE.md)
**后端**
- Flask + SQLAlchemy ORM
- ClickHouse分析型数据库+ MySQL/PostgreSQL事务型数据库

View File

@@ -1,13 +0,0 @@
<!--
IMPORTANT: Please use the following link to create a new issue:
https://www.creative-tim.com/new-issue/argon-dashboard-chakra-pro
**If your issue was not created using the app above, it will be closed immediately.**
-->
<!--
Love Creative Tim? Do you need Angular, React, Vuejs or HTML? You can visit:
👉 https://www.creative-tim.com/bundles
👉 https://www.creative-tim.com
-->

198
README.md
View File

@@ -1,198 +0,0 @@
# vf_react
前端
---
## 📚 重构记录
### 2025-10-30: EventList.js 组件化重构
#### 🎯 重构目标
将 Community 社区页面的 `EventList.js` 组件1095行拆分为多个可复用的子组件提高代码可维护性和复用性。
#### 📊 重构成果
- **重构前**: 1095 行
- **重构后**: 497 行
- **减少**: 598 行 (-54.6%)
---
### 📁 新增目录结构
```
src/views/Community/components/EventCard/
├── index.js (60行) - EventCard 统一入口,智能路由紧凑/详细模式
├── ──────────────────────────────────────────────────────────
│ 原子组件 (Atoms) - 7个基础UI组件
├── ──────────────────────────────────────────────────────────
├── EventTimeline.js (60行) - 时间轴显示组件
│ └── Props: createdAt, timelineStyle, borderColor, minHeight
├── EventImportanceBadge.js (100行) - 重要性等级标签 (S/A/B/C/D)
│ └── Props: importance, showTooltip, showIcon, size
├── EventStats.js (60行) - 统计信息 (浏览/帖子/关注)
│ └── Props: viewCount, postCount, followerCount, size, spacing
├── EventFollowButton.js (40行) - 关注按钮
│ └── Props: isFollowing, followerCount, onToggle, size, showCount
├── EventPriceDisplay.js (130行) - 价格变动显示 (平均/最大/周)
│ └── Props: avgChange, maxChange, weekChange, compact, inline
├── EventDescription.js (60行) - 描述文本 (支持展开/收起)
│ └── Props: description, textColor, minLength, noOfLines
├── EventHeader.js (100行) - 事件标题头部
│ └── Props: title, importance, onTitleClick, linkColor, compact
├── ──────────────────────────────────────────────────────────
│ 组合组件 (Molecules) - 2个卡片组件
├── ──────────────────────────────────────────────────────────
├── CompactEventCard.js (160行) - 紧凑模式事件卡片
│ ├── 使用: EventTimeline, EventHeader, EventStats, EventFollowButton
│ └── Props: event, index, isFollowing, followerCount, callbacks...
└── DetailedEventCard.js (170行) - 详细模式事件卡片
├── 使用: EventTimeline, EventHeader, EventStats, EventFollowButton,
│ EventPriceDisplay, EventDescription
└── Props: event, isFollowing, followerCount, callbacks...
```
**总计**: 10个文件940行代码
---
### 🔧 重构的文件
#### `src/views/Community/components/EventList.js`
**移除的内容**:
-`renderPriceChange` 函数 (~60行)
-`renderCompactEvent` 函数 (~200行)
-`renderDetailedEvent` 函数 (~300行)
-`expandedDescriptions` state展开状态管理移至子组件
- ❌ 冗余的 Chakra UI 导入
**保留的功能**:
- ✅ WebSocket 实时推送
- ✅ 浏览器原生通知
- ✅ 关注状态管理 (followingMap, followCountMap)
- ✅ 分页控制
- ✅ 视图模式切换(紧凑/详细)
- ✅ 推送权限管理
**新增引入**:
```javascript
import EventCard from './EventCard';
```
---
### 🏗️ 架构改进
#### 重构前(单体架构)
```
EventList.js (1095行)
├── 业务逻辑 (WebSocket, 关注, 通知)
├── renderCompactEvent (200行)
│ └── 所有UI代码内联
├── renderDetailedEvent (300行)
│ └── 所有UI代码内联
└── renderPriceChange (60行)
```
#### 重构后(组件化架构)
```
EventList.js (497行) - 容器组件
├── 业务逻辑 (WebSocket, 关注, 通知)
└── 渲染逻辑
└── EventCard (智能路由)
├── CompactEventCard (紧凑模式)
│ ├── EventTimeline
│ ├── EventHeader (compact)
│ ├── EventStats
│ └── EventFollowButton
└── DetailedEventCard (详细模式)
├── EventTimeline
├── EventHeader (detailed)
├── EventStats
├── EventFollowButton
├── EventPriceDisplay
└── EventDescription
```
---
### ✨ 优势
1. **可维护性** ⬆️
- 每个组件职责单一(单一职责原则)
- 代码行数减少 54.6%
- 组件边界清晰,易于理解
2. **可复用性** ⬆️
- 原子组件可在其他页面复用
- 例如EventImportanceBadge 可用于任何需要显示事件等级的地方
3. **可测试性** ⬆️
- 小组件更容易编写单元测试
- 可独立测试每个组件的渲染和交互
4. **性能优化** ⬆️
- React 可以更精确地追踪变化
- 减少不必要的重渲染
- 每个子组件可独立优化useMemo, React.memo
5. **开发效率** ⬆️
- 新增功能时只需修改对应的子组件
- 代码审查更高效
- 降低了代码冲突的概率
---
### 📦 依赖工具函数
本次重构使用了之前提取的工具函数:
```
src/utils/priceFormatters.js (105行)
├── getPriceChangeColor(value) - 获取价格变化文字颜色
├── getPriceChangeBg(value) - 获取价格变化背景颜色
├── getPriceChangeBorderColor(value) - 获取价格变化边框颜色
├── formatPriceChange(value) - 格式化价格为字符串
└── PriceArrow({ value }) - 价格涨跌箭头组件
src/constants/animations.js (72行)
├── pulseAnimation - 脉冲动画S/A级标签
├── fadeIn - 渐入动画
├── slideInUp - 从下往上滑入
├── scaleIn - 缩放进入
└── spin - 旋转动画Loading
```
---
### 🚀 下一步优化计划
Phase 1 已完成,后续可继续优化:
- **Phase 2**: 拆分 StockDetailPanel.js (1067行 → ~250行)
- **Phase 3**: 拆分 InvestmentCalendar.js (827行 → ~200行)
- **Phase 4**: 拆分 MidjourneyHeroSection.js (813行 → ~200行)
- **Phase 5**: 拆分 UnifiedSearchBox.js (679行 → ~180行)
---
### 🔗 相关提交
- `feat: 拆分 EventList.js/提取价格相关工具函数到 utils/priceFormatters.js`
- `feat(EventList): 创建事件卡片原子组件`
- `feat(EventList): 创建事件卡片组合组件`
- `refactor(EventList): 使用组件化架构替换内联渲染函数`
---

Binary file not shown.

Binary file not shown.

2116
app.py

File diff suppressed because it is too large Load Diff

367
app_vx.py
View File

@@ -2463,16 +2463,16 @@ def api_get_events():
order_func = desc if order.lower() == 'desc' else asc
if sort_by == 'hot':
query = query.order_by(order_func(Event.hot_score))
query = query.order_by(order_func(Event.hot_score), desc(Event.created_at))
elif sort_by == 'new':
query = query.order_by(order_func(Event.created_at))
elif sort_by == 'returns':
if return_type == 'avg':
query = query.order_by(order_func(Event.related_avg_chg))
query = query.order_by(order_func(Event.related_avg_chg), desc(Event.created_at))
elif return_type == 'max':
query = query.order_by(order_func(Event.related_max_chg))
query = query.order_by(order_func(Event.related_max_chg), desc(Event.created_at))
elif return_type == 'week':
query = query.order_by(order_func(Event.related_week_chg))
query = query.order_by(order_func(Event.related_week_chg), desc(Event.created_at))
elif sort_by == 'importance':
importance_order = case(
(Event.importance == 'S', 1),
@@ -2482,287 +2482,29 @@ def api_get_events():
else_=5
)
if order.lower() == 'desc':
query = query.order_by(importance_order)
query = query.order_by(importance_order, desc(Event.created_at))
else:
query = query.order_by(desc(importance_order))
query = query.order_by(desc(importance_order), desc(Event.created_at))
elif sort_by == 'view_count':
query = query.order_by(order_func(Event.view_count))
query = query.order_by(order_func(Event.view_count), desc(Event.created_at))
elif sort_by == 'follow' and hasattr(request, 'user') and request.user.is_authenticated:
# 关注的事件排序
query = query.join(EventFollow).filter(
EventFollow.user_id == request.user.id
).order_by(order_func(Event.created_at))
else:
# 兜底排序:始终按时间倒序
query = query.order_by(desc(Event.created_at))
# ==================== 分页查询 ====================
paginated = query.paginate(page=page, per_page=per_page, error_out=False)
# ==================== 批量获取股票行情数据(优化版) ====================
# 1. 收集当前页所有事件的ID
event_ids = [event.id for event in paginated.items]
# 2. 获取所有相关股票
all_related_stocks = {}
if event_ids:
related_stocks = RelatedStock.query.filter(
RelatedStock.event_id.in_(event_ids)
).all()
# 按事件ID分组
for stock in related_stocks:
if stock.event_id not in all_related_stocks:
all_related_stocks[stock.event_id] = []
all_related_stocks[stock.event_id].append(stock)
# 3. 收集所有股票代码
all_stock_codes = []
stock_code_mapping = {} # 清理后的代码 -> 原始代码的映射
for stocks in all_related_stocks.values():
for stock in stocks:
clean_code = stock.stock_code.replace('.SH', '').replace('.SZ', '').replace('.BJ', '')
all_stock_codes.append(clean_code)
stock_code_mapping[clean_code] = stock.stock_code
# 去重
all_stock_codes = list(set(all_stock_codes))
# 4. 批量查询最近7个交易日的数据用于计算日涨跌和周涨跌
stock_price_data = {}
if all_stock_codes:
# 构建SQL查询 - 获取最近7个交易日的数据
codes_str = "'" + "', '".join(all_stock_codes) + "'"
# 获取最近7个交易日的数据
recent_trades_sql = f"""
SELECT
SECCODE,
SECNAME,
F007N as close_price,
F010N as daily_change,
TRADEDATE,
ROW_NUMBER() OVER (PARTITION BY SECCODE ORDER BY TRADEDATE DESC) as rn
FROM ea_trade
WHERE SECCODE IN ({codes_str})
AND F007N IS NOT NULL
AND TRADEDATE >= DATE_SUB(CURDATE(), INTERVAL 10 DAY)
ORDER BY SECCODE, TRADEDATE DESC
"""
result = db.session.execute(text(recent_trades_sql))
# 整理数据
for row in result.fetchall():
sec_code = row[0]
if sec_code not in stock_price_data:
stock_price_data[sec_code] = {
'stock_name': row[1],
'prices': []
}
stock_price_data[sec_code]['prices'].append({
'close_price': float(row[2]) if row[2] else 0,
'daily_change': float(row[3]) if row[3] else 0,
'trade_date': row[4],
'rank': row[5]
})
# 5. 计算日涨跌和周涨跌
stock_changes = {}
for sec_code, data in stock_price_data.items():
prices = data['prices']
# 最新日涨跌第1条记录
daily_change = 0
if prices and prices[0]['rank'] == 1:
daily_change = prices[0]['daily_change']
# 计算周涨跌(最新价 vs 5个交易日前的价格
week_change = 0
if len(prices) >= 2:
latest_price = prices[0]['close_price']
# 找到第5个交易日的数据如果有
week_ago_price = None
for price_data in prices:
if price_data['rank'] >= 5:
week_ago_price = price_data['close_price']
break
# 如果没有第5天的数据使用最早的数据
if week_ago_price is None and len(prices) > 1:
week_ago_price = prices[-1]['close_price']
if week_ago_price and week_ago_price > 0:
week_change = (latest_price - week_ago_price) / week_ago_price * 100
stock_changes[sec_code] = {
'stock_name': data['stock_name'],
'daily_change': daily_change,
'week_change': week_change
}
# ==================== 获取整体统计信息(应用所有筛选条件) ====================
overall_distribution = {
'limit_down': 0,
'down_over_5': 0,
'down_5_to_1': 0,
'down_within_1': 0,
'flat': 0,
'up_within_1': 0,
'up_1_to_5': 0,
'up_over_5': 0,
'limit_up': 0
}
# 使用当前筛选条件的query但不应用分页限制获取所有符合条件的事件
# 这样统计数据会跟随用户的筛选条件变化
all_filtered_events = query.limit(1000).all() # 限制最多1000个事件避免查询过慢
week_event_ids = [e.id for e in all_filtered_events]
if week_event_ids:
# 获取这些事件的所有关联股票
week_related_stocks = RelatedStock.query.filter(
RelatedStock.event_id.in_(week_event_ids)
).all()
# 按事件ID分组
week_stocks_by_event = {}
for stock in week_related_stocks:
if stock.event_id not in week_stocks_by_event:
week_stocks_by_event[stock.event_id] = []
week_stocks_by_event[stock.event_id].append(stock)
# 收集所有股票代码(用于批量查询行情)
week_stock_codes = []
week_code_mapping = {}
for stocks in week_stocks_by_event.values():
for stock in stocks:
clean_code = stock.stock_code.replace('.SH', '').replace('.SZ', '').replace('.BJ', '')
week_stock_codes.append(clean_code)
week_code_mapping[clean_code] = stock.stock_code
week_stock_codes = list(set(week_stock_codes))
# 批量查询这些股票的最新行情数据
week_stock_changes = {}
if week_stock_codes:
codes_str = "'" + "', '".join(week_stock_codes) + "'"
recent_trades_sql = f"""
SELECT
SECCODE,
SECNAME,
F010N as daily_change,
ROW_NUMBER() OVER (PARTITION BY SECCODE ORDER BY TRADEDATE DESC) as rn
FROM ea_trade
WHERE SECCODE IN ({codes_str})
AND F010N IS NOT NULL
AND TRADEDATE >= DATE_SUB(CURDATE(), INTERVAL 3 DAY)
ORDER BY SECCODE, TRADEDATE DESC
"""
result = db.session.execute(text(recent_trades_sql))
for row in result.fetchall():
sec_code = row[0]
if row[3] == 1: # 只取最新的数据rn=1
week_stock_changes[sec_code] = {
'stock_name': row[1],
'daily_change': float(row[2]) if row[2] else 0
}
# 按事件统计平均涨跌幅分布
event_avg_changes = {}
for event in all_filtered_events:
event_stocks = week_stocks_by_event.get(event.id, [])
if not event_stocks:
continue
total_change = 0
valid_count = 0
for stock in event_stocks:
clean_code = stock.stock_code.replace('.SH', '').replace('.SZ', '').replace('.BJ', '')
if clean_code in week_stock_changes:
daily_change = week_stock_changes[clean_code]['daily_change']
total_change += daily_change
valid_count += 1
if valid_count > 0:
avg_change = total_change / valid_count
event_avg_changes[event.id] = avg_change
# 统计事件平均涨跌幅的分布
for event_id, avg_change in event_avg_changes.items():
# 对于事件平均涨幅,不使用涨跌停分类,使用通用分类
if avg_change <= -10:
overall_distribution['limit_down'] += 1
elif avg_change >= 10:
overall_distribution['limit_up'] += 1
elif avg_change > 5:
overall_distribution['up_over_5'] += 1
elif avg_change > 1:
overall_distribution['up_1_to_5'] += 1
elif avg_change > 0.1:
overall_distribution['up_within_1'] += 1
elif avg_change >= -0.1:
overall_distribution['flat'] += 1
elif avg_change > -1:
overall_distribution['down_within_1'] += 1
elif avg_change > -5:
overall_distribution['down_5_to_1'] += 1
else:
overall_distribution['down_over_5'] += 1
# ==================== 构建响应数据 ====================
events_data = []
for event in paginated.items:
event_stocks = all_related_stocks.get(event.id, [])
stocks_data = []
total_daily_change = 0
max_daily_change = -999
total_week_change = 0
max_week_change = -999
valid_stocks_count = 0
# 处理每个股票的数据
for stock in event_stocks:
clean_code = stock.stock_code.replace('.SH', '').replace('.SZ', '').replace('.BJ', '')
stock_info = stock_changes.get(clean_code, {})
daily_change = stock_info.get('daily_change', 0)
week_change = stock_info.get('week_change', 0)
if stock_info:
total_daily_change += daily_change
max_daily_change = max(max_daily_change, daily_change)
total_week_change += week_change
max_week_change = max(max_week_change, week_change)
valid_stocks_count += 1
stocks_data.append({
"stock_code": stock.stock_code,
"stock_name": stock.stock_name,
"sector": stock.sector,
"week_change": round(week_change, 2),
"daily_change": round(daily_change, 2)
})
avg_daily_change = total_daily_change / valid_stocks_count if valid_stocks_count > 0 else 0
avg_week_change = total_week_change / valid_stocks_count if valid_stocks_count > 0 else 0
if max_daily_change == -999:
max_daily_change = 0
if max_week_change == -999:
max_week_change = 0
# 构建事件数据
# 构建事件数据(保持原有结构,个股信息和统计置空)
event_dict = {
'id': event.id,
'title': event.title,
@@ -2774,20 +2516,21 @@ def api_get_events():
'updated_at': event.updated_at.isoformat() if event.updated_at else None,
'start_time': event.start_time.isoformat() if event.start_time else None,
'end_time': event.end_time.isoformat() if event.end_time else None,
'related_stocks': stocks_data,
# 个股信息(置空)
'related_stocks': [],
# 股票统计(置空或使用数据库字段)
'stocks_stats': {
'stocks_count': len(event_stocks),
'valid_stocks_count': valid_stocks_count,
# 周涨跌统计
'avg_week_change': round(avg_week_change, 2),
'max_week_change': round(max_week_change, 2),
# 日涨跌统计
'avg_daily_change': round(avg_daily_change, 2),
'max_daily_change': round(max_daily_change, 2)
'stocks_count': 10,
'valid_stocks_count': 0,
# 使用数据库字段的涨跌幅
'avg_week_change': round(event.related_week_chg, 2) if event.related_week_chg else 0,
'max_week_change': round(event.related_max_chg, 2) if event.related_max_chg else 0,
'avg_daily_change': round(event.related_avg_chg, 2) if event.related_avg_chg else 0,
'max_daily_change': round(event.related_max_chg, 2) if event.related_max_chg else 0
}
}
# 统计信息
# 统计信息(可选)
if include_stats:
event_dict.update({
'hot_score': event.hot_score,
@@ -2801,7 +2544,7 @@ def api_get_events():
'trending_score': event.trending_score,
})
# 创建者信息
# 创建者信息(可选)
if include_creator:
event_dict['creator'] = {
'id': event.creator.id if event.creator else None,
@@ -2815,24 +2558,18 @@ def api_get_events():
event_dict['keywords'] = event.keywords if isinstance(event.keywords, list) else []
event_dict['related_industries'] = event.related_industries
# 包含统计信息
# 包含统计信息(可选,置空)
if include_stats:
event_dict['stats'] = {
'related_stocks_count': len(event_stocks),
'historical_events_count': 0, # 需要额外查询
'related_data_count': 0, # 需要额外查询
'related_concepts_count': 0 # 需要额外查询
'related_stocks_count': 10,
'historical_events_count': 0,
'related_data_count': 0,
'related_concepts_count': 0
}
# 包含关联数据
# 包含关联数据(可选,已置空)
if include_related_data:
event_dict['related_stocks'] = [{
'id': stock.id,
'stock_code': stock.stock_code,
'stock_name': stock.stock_name,
'sector': stock.sector,
'correlation': float(stock.correlation) if stock.correlation else 0
} for stock in event_stocks[:5]] # 限制返回5个
event_dict['related_stocks'] = []
events_data.append(event_dict)
@@ -2860,7 +2597,7 @@ def api_get_events():
applied_filters['search_query'] = search_query
applied_filters['search_type'] = search_type
# ==================== 返回结果(保持完全兼容) ====================
# ==================== 返回结果(保持完全兼容,统计数据置空 ====================
return jsonify({
'success': True,
@@ -2885,12 +2622,30 @@ def api_get_events():
'order': order
}
},
# 整体股票涨跌幅分布统计
# 整体股票涨跌幅分布统计(置空)
'overall_stats': {
'total_stocks': len(all_stocks_for_stats) if 'all_stocks_for_stats' in locals() else 0,
'change_distribution': overall_distribution,
'total_stocks': 0,
'change_distribution': {
'limit_down': 0,
'down_over_5': 0,
'down_5_to_1': 0,
'down_within_1': 0,
'flat': 0,
'up_within_1': 0,
'up_1_to_5': 0,
'up_over_5': 0,
'limit_up': 0
},
'change_distribution_percentages': {
k: v for k, v in overall_distribution.items()
'limit_down': 0,
'down_over_5': 0,
'down_5_to_1': 0,
'down_within_1': 0,
'flat': 0,
'up_within_1': 0,
'up_1_to_5': 0,
'up_over_5': 0,
'limit_up': 0
}
}
}
@@ -2964,13 +2719,12 @@ def get_calendar_event_counts():
# 修改查询以仅统计type为event的事件数量
query = """
SELECT DATE (calendar_time) as date, COUNT (*) as count
FROM future_events
WHERE calendar_time BETWEEN :start_date \
AND :end_date
AND type = 'event'
GROUP BY DATE (calendar_time) \
"""
SELECT DATE(calendar_time) as date, COUNT(*) as count
FROM future_events
WHERE calendar_time BETWEEN :start_date AND :end_date
AND type = 'event'
GROUP BY DATE(calendar_time)
"""
result = db.session.execute(text(query), {
'start_date': start_date,
@@ -2987,7 +2741,8 @@ def get_calendar_event_counts():
return jsonify(events)
except Exception as e:
return jsonify({'error': str(e)}), 500
app.logger.error(f"获取日历事件统计出错: {str(e)}", exc_info=True)
return jsonify({'error': str(e), 'error_type': type(e).__name__}), 500
def get_full_avatar_url(avatar_url):

6352
app_vx_copy1.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -300,8 +300,8 @@
"tags": [
"分类树"
],
"summary": "获取特定节点及其子树",
"description": "根据路径获取树中的特定节点及其所有子节点。\n\n## 使用场景\n- 懒加载:用户点击节点时动态加载子节点\n- 子树查询:获取某个分类下的所有数据\n- 面包屑导航:根据路径定位节点\n\n## 路径格式\n使用竖线(|)分隔层级,例如:\n- 一级: \"\"\n- 二级: \"钴|钴化合物\"\n- 三级: \"钴|钴化合物|硫酸钴\"\n",
"summary": "获取节点及其直接子节点(第一层)",
"description": "根据路径获取树中的特定节点,**只返回直接子节点**(不包括孙节点及更深层级)。\n\n## 功能特点\n- **分层懒加载**: 每次只返回下一层,避免一次性加载过多数据\n- **性能优化**: 减少数据传输量,提升响应速度\n- **无限层级**: 支持任意深度的节点展开\n\n## 使用场景\n- 用户点击节点时,动态加载该节点的直接子节点\n- 逐层展开树形结构\n- 按需加载,提升用户体验\n\n## 返回数据说明\n- 返回该节点的基本信息\n- 返回该节点的直接子节点列表\n- 每个子节点的`children`数组为空`[]`\n- 通过`has_children`字段判断子节点是否可继续展开\n\n## 路径格式\n使用竖线(|)分隔层级,例如:\n- 第1层: \"小金属\"\n- 第2层: \"小金属|钴\"\n- 第3层: \"小金属|钴|钴化合物\"\n",
"operationId": "getCategoryTreeNode",
"parameters": [
{
@@ -337,15 +337,75 @@
"schema": {
"$ref": "#/components/schemas/TreeNode"
},
"example": {
"name": "硫酸钴",
"path": "钴|钴化合物|硫酸钴",
"level": 3,
"children": [
{
"examples": {
"展开第1层节点": {
"value": {
"name": "小金属",
"path": "小金属",
"level": 1,
"has_children": true,
"children": [
{
"name": "钴",
"path": "小金属|钴",
"level": 2,
"has_children": true,
"children": [],
"metrics": []
},
{
"name": "锂",
"path": "小金属|锂",
"level": 2,
"has_children": true,
"children": [],
"metrics": []
},
{
"name": "镍",
"path": "小金属|镍",
"level": 2,
"has_children": true,
"children": [],
"metrics": []
}
],
"metrics": []
}
},
"展开第2层节点": {
"value": {
"name": "钴",
"path": "小金属|钴",
"level": 2,
"has_children": true,
"children": [
{
"name": "钴化合物",
"path": "小金属|钴|钴化合物",
"level": 3,
"has_children": true,
"children": [],
"metrics": []
},
{
"name": "钴矿",
"path": "小金属|钴|钴矿",
"level": 3,
"has_children": true,
"children": [],
"metrics": []
}
],
"metrics": []
}
},
"叶子节点(含指标)": {
"value": {
"name": "产量",
"path": "钴|钴化合物|硫酸钴|产量",
"level": 4,
"path": "小金属|钴|钴化合物|硫酸钴|产量",
"level": 5,
"has_children": false,
"children": [],
"metrics": [
{
@@ -358,7 +418,7 @@
}
]
}
]
}
}
}
}
@@ -373,7 +433,7 @@
"examples": {
"节点不存在": {
"value": {
"detail": "未找到路径 '|不存在的节点' 对应的节点"
"detail": "未找到路径 '小金属|不存在的节点' 对应的节点"
}
},
"数据源不存在": {

View File

@@ -110,7 +110,7 @@ class SearchRequest(BaseModel):
semantic_weight: Optional[float] = Field(None, ge=0.0, le=1.0, description="语义搜索权重(0-1)None表示自动计算")
filter_stocks: Optional[List[str]] = Field(None, description="过滤特定股票代码或名称")
trade_date: Optional[date] = Field(None, description="交易日期格式YYYY-MM-DD默认返回最新日期数据")
sort_by: str = Field("change_pct", description="排序方式: change_pct, _score, stock_count, concept_name")
sort_by: str = Field("change_pct", description="排序方式: change_pct, _score, stock_count, concept_name, added_date")
use_knn: bool = Field(True, description="是否使用KNN搜索优化语义搜索")
@@ -548,12 +548,12 @@ async def search_concepts(request: SearchRequest):
# 已经在generate_embedding中记录了详细日志这里只调整语义权重
semantic_weight = 0
# 【关键修改】:如果按涨跌幅排序,需要获取更多结果
# 【关键修改】:如果按涨跌幅或添加日期排序,需要获取更多结果
effective_search_size = request.search_size
if request.sort_by == "change_pct":
# 按涨跌幅排序时,获取更多结果以确保排序准确性
if request.sort_by in ["change_pct", "added_date"]:
# 按涨跌幅或添加日期排序时,获取更多结果以确保排序准确性
effective_search_size = min(1000, request.search_size * 10) # 最多获取1000个
logger.info(f"Using expanded search size {effective_search_size} for change_pct sorting")
logger.info(f"Using expanded search size {effective_search_size} for {request.sort_by} sorting")
# 构建查询体
search_body = {}
@@ -721,6 +721,14 @@ async def search_concepts(request: SearchRequest):
all_results.sort(key=lambda x: x.stock_count, reverse=True)
elif request.sort_by == "concept_name":
all_results.sort(key=lambda x: x.concept)
elif request.sort_by == "added_date":
# 按添加日期排序(降序 - 最新的在前)
all_results.sort(
key=lambda x: (
x.happened_times[0] if x.happened_times and len(x.happened_times) > 0 else '1900-01-01'
),
reverse=True
)
# _score排序已经由ES处理
# 计算分页

View File

@@ -76,7 +76,7 @@ module.exports = {
},
// 日期/日历库
calendar: {
test: /[\\/]node_modules[\\/](dayjs|date-fns|@fullcalendar|react-big-calendar)[\\/]/,
test: /[\\/]node_modules[\\/](dayjs|date-fns|@fullcalendar)[\\/]/,
name: 'calendar-lib',
priority: 18,
reuseExistingChunk: true,
@@ -284,9 +284,19 @@ module.exports = {
},
// 代理配置:将 /api 请求代理到后端服务器
// 注意Mock 模式下禁用 proxy,让 MSW 拦截请求
...(isMockMode() ? {} : {
proxy: {
// 注意Mock 模式下禁用 /api 和 /concept-api,让 MSW 拦截请求
// 但 /bytedesk 始终启用(客服系统不走 Mock
proxy: {
'/bytedesk': {
target: 'https://valuefrontier.cn', // 统一使用生产环境 Nginx 代理
changeOrigin: true,
secure: false, // 开发环境禁用 HTTPS 严格验证
logLevel: 'debug',
ws: true, // 支持 WebSocket
// 不使用 pathRewrite保留 /bytedesk 前缀,让生产 Nginx 处理
},
// Mock 模式下禁用其他代理
...(isMockMode() ? {} : {
'/api': {
target: 'http://49.232.185.254:5001',
changeOrigin: true,
@@ -300,15 +310,7 @@ module.exports = {
logLevel: 'debug',
pathRewrite: { '^/concept-api': '' },
},
'/bytedesk': {
target: 'https://valuefrontier.cn', // 统一使用生产环境 Nginx 代理
changeOrigin: true,
secure: false, // 开发环境禁用 HTTPS 严格验证
logLevel: 'debug',
ws: true, // 支持 WebSocket
// 不使用 pathRewrite保留 /bytedesk 前缀,让生产 Nginx 处理
},
},
}),
}),
},
},
};

View File

@@ -1,614 +0,0 @@
# PostHog Dashboard 配置指南
## 📊 目的
本指南帮助你在PostHog中配置关键的分析Dashboard和Insights快速获得有价值的用户行为洞察。
---
## 🎯 推荐Dashboard列表
### 1. 📈 核心指标Dashboard
**用途**: 监控产品整体健康度
### 2. 🔄 用户留存Dashboard
**用途**: 分析用户留存和流失
### 3. 💰 收入转化Dashboard
**用途**: 监控付费转化漏斗
### 4. 🎨 功能使用Dashboard
**用途**: 了解功能受欢迎程度
### 5. 🔍 搜索行为Dashboard
**用途**: 优化搜索体验
---
## 📈 Dashboard 1: 核心指标
### Insight 1.1: 每日活跃用户DAU
**类型**: Trends
**事件**: `$pageview`
**时间范围**: 过去30天
**分组**: 按日
**配置**:
```
Event: $pageview
Unique users
Date range: Last 30 days
Interval: Day
```
### Insight 1.2: 新用户注册趋势
**类型**: Trends
**事件**: `USER_SIGNED_UP`
**时间范围**: 过去30天
**配置**:
```
Event: USER_SIGNED_UP
Count of events
Date range: Last 30 days
Interval: Day
Breakdown: signup_method
```
### Insight 1.3: 用户登录方式分布
**类型**: Pie Chart
**事件**: `USER_LOGGED_IN`
**时间范围**: 过去7天
**配置**:
```
Event: USER_LOGGED_IN
Count of events
Date range: Last 7 days
Breakdown: login_method
Visualization: Pie
```
### Insight 1.4: 最受欢迎的页面
**类型**: Table
**事件**: `$pageview`
**时间范围**: 过去7天
**配置**:
```
Event: $pageview
Count of events
Date range: Last 7 days
Breakdown: $current_url
Order: Descending
Limit: Top 10
```
### Insight 1.5: 平台分布
**类型**: Bar Chart
**事件**: `$pageview`
**时间范围**: 过去30天
**配置**:
```
Event: $pageview
Unique users
Date range: Last 30 days
Breakdown: $os
Visualization: Bar
```
---
## 🔄 Dashboard 2: 用户留存
### Insight 2.1: 用户留存曲线
**类型**: Retention
**初始事件**: `USER_SIGNED_UP`
**返回事件**: `$pageview`
**配置**:
```
Cohort defining event: USER_SIGNED_UP
Returning event: $pageview
Period: Daily
Date range: Last 8 weeks
```
### Insight 2.2: 功能留存率
**类型**: Retention
**初始事件**: 各功能首次使用事件
**返回事件**: 各功能再次使用
**配置**:
```
Cohort defining event: TRADING_SIMULATION_ENTERED
Returning event: TRADING_SIMULATION_ENTERED
Period: Weekly
Date range: Last 12 weeks
```
### Insight 2.3: 社区互动留存
**类型**: Retention
**初始事件**: `Community Page Viewed`
**返回事件**: `NEWS_ARTICLE_CLICKED`
**配置**:
```
Cohort defining event: Community Page Viewed
Returning event: NEWS_ARTICLE_CLICKED
Period: Daily
Date range: Last 30 days
```
### Insight 2.4: 活跃用户分层
**类型**: Trends
**多个事件**: 按活跃度分类
**配置**:
```
Event 1: $pageview (filter: >= 20 events in last 7 days)
Event 2: $pageview (filter: 10-19 events in last 7 days)
Event 3: $pageview (filter: 3-9 events in last 7 days)
Event 4: $pageview (filter: 1-2 events in last 7 days)
Date range: Last 30 days
Unique users
```
---
## 💰 Dashboard 3: 收入转化
### Insight 3.1: 付费转化漏斗
**类型**: Funnel
**步骤**:
1. SUBSCRIPTION_PAGE_VIEWED
2. Pricing Plan Selected
3. PAYMENT_INITIATED
4. PAYMENT_SUCCESSFUL
5. SUBSCRIPTION_CREATED
**配置**:
```
Funnel step 1: SUBSCRIPTION_PAGE_VIEWED
Funnel step 2: Pricing Plan Selected
Funnel step 3: PAYMENT_INITIATED
Funnel step 4: PAYMENT_SUCCESSFUL
Funnel step 5: SUBSCRIPTION_CREATED
Conversion window: 1 hour
Date range: Last 30 days
```
### Insight 3.2: 付费墙转化率
**类型**: Funnel
**步骤**:
1. PAYWALL_SHOWN
2. PAYWALL_UPGRADE_CLICKED
3. SUBSCRIPTION_PAGE_VIEWED
4. PAYMENT_SUCCESSFUL
**配置**:
```
Funnel step 1: PAYWALL_SHOWN
Funnel step 2: PAYWALL_UPGRADE_CLICKED
Funnel step 3: SUBSCRIPTION_PAGE_VIEWED
Funnel step 4: PAYMENT_SUCCESSFUL
Breakdown: feature (付费墙触发功能)
Date range: Last 30 days
```
### Insight 3.3: 定价方案选择分布
**类型**: Pie Chart
**事件**: `Pricing Plan Selected`
**配置**:
```
Event: Pricing Plan Selected
Count of events
Breakdown: plan_name
Date range: Last 30 days
Visualization: Pie
```
### Insight 3.4: 计费周期偏好
**类型**: Bar Chart
**事件**: `Pricing Plan Selected`
**配置**:
```
Event: Pricing Plan Selected
Count of events
Breakdown: billing_cycle
Date range: Last 30 days
Visualization: Bar
```
### Insight 3.5: 支付成功率
**类型**: Trends (Formula)
**计算**: (PAYMENT_SUCCESSFUL / PAYMENT_INITIATED) * 100
**配置**:
```
Series A: PAYMENT_SUCCESSFUL (Count)
Series B: PAYMENT_INITIATED (Count)
Formula: (A / B) * 100
Date range: Last 30 days
Interval: Day
```
### Insight 3.6: 订阅收入趋势
**类型**: Trends
**事件**: `SUBSCRIPTION_CREATED`
**配置**:
```
Event: SUBSCRIPTION_CREATED
Sum of property: amount
Date range: Last 90 days
Interval: Week
```
### Insight 3.7: 支付失败原因分析
**类型**: Table
**事件**: `PAYMENT_FAILED`
**配置**:
```
Event: PAYMENT_FAILED
Count of events
Breakdown: error_reason
Date range: Last 30 days
Order: Descending
```
---
## 🎨 Dashboard 4: 功能使用
### Insight 4.1: 功能使用频率排名
**类型**: Table
**多个事件**: 各功能的关键事件
**配置**:
```
Events:
- Community Page Viewed
- EVENT_DETAIL_VIEWED
- DASHBOARD_CENTER_VIEWED
- TRADING_SIMULATION_ENTERED
- STOCK_OVERVIEW_VIEWED
Count of events
Date range: Last 7 days
Order: Descending
```
### Insight 4.2: 新闻浏览趋势
**类型**: Trends
**事件**: `NEWS_ARTICLE_CLICKED`
**配置**:
```
Event: NEWS_ARTICLE_CLICKED
Count of events
Date range: Last 30 days
Interval: Day
Breakdown: importance (按重要性分组)
```
### Insight 4.3: 搜索使用趋势
**类型**: Trends
**事件**: `SEARCH_QUERY_SUBMITTED`
**配置**:
```
Event: SEARCH_QUERY_SUBMITTED
Count of events
Date range: Last 30 days
Interval: Day
Breakdown: context
```
### Insight 4.4: 模拟盘交易活跃度
**类型**: Trends
**事件**: `Simulation Order Placed`
**配置**:
```
Event: Simulation Order Placed
Count of events
Date range: Last 30 days
Interval: Day
Breakdown: order_type (买入/卖出)
```
### Insight 4.5: 社交互动参与度
**类型**: Trends (Stacked)
**多个事件**:
- Comment Added
- Comment Liked
- CONTENT_SHARED
**配置**:
```
Event 1: Comment Added
Event 2: Comment Liked
Event 3: CONTENT_SHARED
Count of events
Date range: Last 30 days
Interval: Day
Visualization: Area (Stacked)
```
### Insight 4.6: 个人资料完善度
**类型**: Funnel
**步骤**:
1. USER_SIGNED_UP
2. PROFILE_UPDATED
3. Avatar Uploaded
4. Account Bound
**配置**:
```
Funnel step 1: USER_SIGNED_UP
Funnel step 2: PROFILE_UPDATED
Funnel step 3: Avatar Uploaded
Funnel step 4: Account Bound
Date range: Last 30 days
```
---
## 🔍 Dashboard 5: 搜索行为
### Insight 5.1: 搜索量趋势
**类型**: Trends
**事件**: `SEARCH_QUERY_SUBMITTED`
**配置**:
```
Event: SEARCH_QUERY_SUBMITTED
Count of events
Date range: Last 30 days
Interval: Day
```
### Insight 5.2: 搜索无结果率
**类型**: Trends (Formula)
**计算**: (SEARCH_NO_RESULTS / SEARCH_QUERY_SUBMITTED) * 100
**配置**:
```
Series A: SEARCH_NO_RESULTS (Count)
Series B: SEARCH_QUERY_SUBMITTED (Count)
Formula: (A / B) * 100
Date range: Last 30 days
Interval: Day
```
### Insight 5.3: 热门搜索词
**类型**: Table
**事件**: `SEARCH_QUERY_SUBMITTED`
**配置**:
```
Event: SEARCH_QUERY_SUBMITTED
Count of events
Breakdown: query
Date range: Last 7 days
Order: Descending
Limit: Top 20
```
### Insight 5.4: 搜索结果点击率
**类型**: Funnel
**步骤**:
1. SEARCH_QUERY_SUBMITTED
2. SEARCH_RESULT_CLICKED
**配置**:
```
Funnel step 1: SEARCH_QUERY_SUBMITTED
Funnel step 2: SEARCH_RESULT_CLICKED
Breakdown: context
Date range: Last 30 days
```
### Insight 5.5: 搜索筛选使用
**类型**: Table
**事件**: `SEARCH_FILTER_APPLIED`
**配置**:
```
Event: SEARCH_FILTER_APPLIED
Count of events
Breakdown: filter_type
Date range: Last 30 days
Order: Descending
```
---
## 👥 推荐Cohorts用户分组
### Cohort 1: 活跃用户
**条件**:
```
用户在过去7天内执行了
$pageview (至少5次)
```
### Cohort 2: 付费用户
**条件**:
```
用户执行过:
SUBSCRIPTION_CREATED
并且
subscription_tier 不等于 'free'
```
### Cohort 3: 社区活跃用户
**条件**:
```
用户在过去30天内执行了
Comment Added (至少1次)
Comment Liked (至少3次)
```
### Cohort 4: 流失风险用户
**条件**:
```
用户满足:
上次访问时间 > 7天前
并且
历史访问次数 >= 5次
```
### Cohort 5: 高价值潜在用户
**条件**:
```
用户在过去30天内
PAYWALL_SHOWN (至少2次)
并且
未执行过 SUBSCRIPTION_CREATED
并且
$pageview (至少10次)
```
### Cohort 6: 新用户(激活中)
**条件**:
```
用户执行过:
USER_SIGNED_UP (在过去7天内)
```
---
## 🎯 推荐Actions动作定义
### Action 1: 深度参与
**定义**: 用户在单次会话中执行了多个关键操作
**包含事件**:
- NEWS_ARTICLE_CLICKED (至少2次)
- EVENT_DETAIL_VIEWED (至少1次)
- Comment Added 或 Comment Liked (至少1次)
### Action 2: 付费意向
**定义**: 用户展现付费兴趣
**包含事件**:
- PAYWALL_SHOWN
- PAYWALL_UPGRADE_CLICKED
- SUBSCRIPTION_PAGE_VIEWED
### Action 3: 模拟盘活跃
**定义**: 用户积极使用模拟盘
**包含事件**:
- TRADING_SIMULATION_ENTERED
- Simulation Order Placed (至少1次)
- Simulation Holdings Viewed
---
## 📱 配置步骤
### 创建Dashboard
1. 登录PostHog
2. 左侧菜单选择 "Dashboards"
3. 点击 "New dashboard"
4. 输入Dashboard名称如"核心指标Dashboard"
5. 点击 "Create"
### 添加Insight
1. 在Dashboard页面点击 "Add insight"
2. 选择Insight类型Trends/Funnel/Retention等
3. 配置事件和参数
4. 点击 "Save & add to dashboard"
### 配置Cohort
1. 左侧菜单选择 "Cohorts"
2. 点击 "New cohort"
3. 设置Cohort名称
4. 添加筛选条件
5. 点击 "Save"
### 配置Action
1. 左侧菜单选择 "Data management" -> "Actions"
2. 点击 "New action"
3. 选择 "From event or pageview"
4. 添加匹配条件
5. 点击 "Save"
---
## 🔔 推荐Alerts告警配置
### Alert 1: 支付成功率下降
**条件**: 支付成功率 < 80%
**检查频率**: 每小时
**通知方式**: Email + Slack
### Alert 2: 搜索无结果率过高
**条件**: 搜索无结果率 > 30%
**检查频率**: 每天
**通知方式**: Email
### Alert 3: 新用户注册激增
**条件**: 新注册用户数 > 正常值的2倍
**检查频率**: 每小时
**通知方式**: Slack
### Alert 4: 系统异常
**条件**: 错误事件数 > 100/小时
**检查频率**: 每15分钟
**通知方式**: Email + Slack + PagerDuty
---
## 💡 使用建议
### 日常监控
**建议查看频率**: 每天
**关注Dashboard**:
- 核心指标Dashboard
- 收入转化Dashboard
### 周度回顾
**建议查看频率**: 每周一
**关注Dashboard**:
- 用户留存Dashboard
- 功能使用Dashboard
### 月度分析
**建议查看频率**: 每月初
**关注Dashboard**:
- 所有Dashboard
- Cohorts分析
- Retention详细报告
### 决策支持
**使用场景**:
- 功能优先级排序 → 查看功能使用Dashboard
- 转化率优化 → 查看收入转化Dashboard
- 用户流失分析 → 查看用户留存Dashboard
- 搜索体验优化 → 查看搜索行为Dashboard
---
## 📊 高级分析技巧
### 1. Funnel分解分析
在漏斗的每一步添加Breakdown分析不同用户群的转化差异
- 按 subscription_tier 分解
- 按 signup_method 分解
- 按 $os 分解
### 2. Cohort对比
创建多个Cohort在Insights中对比不同群体的行为
- 付费用户 vs 免费用户
- 新用户 vs 老用户
- 活跃用户 vs 流失用户
### 3. Path Analysis
使用Paths功能分析用户旅程
- 从注册到首次付费的路径
- 从首页到核心功能的路径
- 流失用户的最后操作路径
### 4. 时间对比
使用 "Compare to previous period" 功能:
- 本周 vs 上周
- 本月 vs 上月
- 节假日 vs 平常
---
## 🔗 相关资源
- [PostHog Dashboard文档](https://posthog.com/docs/user-guides/dashboards)
- [PostHog Insights文档](https://posthog.com/docs/user-guides/insights)
- [PostHog Cohorts文档](https://posthog.com/docs/user-guides/cohorts)
- [TRACKING_VALIDATION_CHECKLIST.md](./TRACKING_VALIDATION_CHECKLIST.md) - 验证清单
---
**文档版本**: v1.0
**最后更新**: 2025-10-29
**维护者**: 数据分析团队

View File

@@ -55,19 +55,40 @@ HTTP_CLIENT = httpx.AsyncClient(timeout=60.0)
# ==================== Agent系统配置 ====================
# Kimi 配置 - 用于计划制定和深度推理
KIMI_CONFIG = {
"api_key": "sk-TzB4VYJfCoXGcGrGMiewukVRzjuDsbVCkaZXi2LvkS8s60E5",
"base_url": "https://api.moonshot.cn/v1",
"model": "kimi-k2-thinking", # 思考模型
# ==================== 多模型配置 ====================
# 模型配置字典(支持动态切换)
MODEL_CONFIGS = {
"kimi-k2": {
"api_key": "sk-TzB4VYJfCoXGcGrGMiewukVRzjuDsbVCkaZXi2LvkS8s60E5",
"base_url": "https://api.moonshot.cn/v1",
"model": "moonshot-v1-8k", # 快速模型
},
"kimi-k2-thinking": {
"api_key": "sk-TzB4VYJfCoXGcGrGMiewukVRzjuDsbVCkaZXi2LvkS8s60E5",
"base_url": "https://api.moonshot.cn/v1",
"model": "kimi-k2-thinking", # 深度思考模型
},
"glm-4.6": {
"api_key": "", # 需要配置智谱AI密钥
"base_url": "https://open.bigmodel.cn/api/paas/v4",
"model": "glm-4",
},
"deepmoney": {
"api_key": "", # 空值
"base_url": "http://111.62.35.50:8000/v1",
"model": "deepmoney",
},
"gemini-3": {
"api_key": "", # 需要配置Google API密钥
"base_url": "https://generativelanguage.googleapis.com/v1",
"model": "gemini-pro",
},
}
# DeepMoney 配置 - 用于新闻总结
DEEPMONEY_CONFIG = {
"api_key": "", # 空值
"base_url": "http://111.62.35.50:8000/v1",
"model": "deepmoney",
}
# 保持向后兼容的配置(默认使用 kimi-k2-thinking
KIMI_CONFIG = MODEL_CONFIGS["kimi-k2-thinking"]
DEEPMONEY_CONFIG = MODEL_CONFIGS["deepmoney"]
# ==================== MCP协议数据模型 ====================
@@ -143,6 +164,8 @@ class AgentChatRequest(BaseModel):
user_avatar: Optional[str] = None # 用户头像URL
subscription_type: Optional[str] = None # 用户订阅类型free/pro/max
session_id: Optional[str] = None # 会话ID如果为空则创建新会话
model: Optional[str] = "kimi-k2-thinking" # 选择的模型kimi-k2, kimi-k2-thinking, glm-4.6, deepmoney, gemini-3
tools: Optional[List[str]] = None # 选择的工具列表工具名称数组如果为None则使用全部工具
# ==================== MCP工具定义 ====================
@@ -1579,6 +1602,7 @@ class MCPAgentIntegrated:
user_nickname: str = None,
user_avatar: str = None,
cookies: dict = None,
model_config: dict = None, # 新增:动态模型配置
) -> AsyncGenerator[str, None]:
"""主流程(流式输出)- 逐步返回执行结果"""
logger.info(f"[Agent Stream] 处理查询: {user_query}")
@@ -1586,11 +1610,24 @@ class MCPAgentIntegrated:
# 将 cookies 存储为实例属性,供工具调用时使用
self.cookies = cookies or {}
# 如果传入了自定义模型配置,使用自定义配置,否则使用默认的 Kimi
if model_config:
planning_client = OpenAI(
api_key=model_config["api_key"],
base_url=model_config["base_url"],
)
planning_model = model_config["model"]
logger.info(f"[Agent Stream] 使用自定义模型: {planning_model}")
else:
planning_client = self.kimi_client
planning_model = self.kimi_model
logger.info(f"[Agent Stream] 使用默认模型: {planning_model}")
try:
# 发送开始事件
yield self._format_sse("status", {"stage": "start", "message": "开始处理查询"})
# 阶段1: Kimi 制定计划(流式,带 DeepMoney 备选)
# 阶段1: 使用选中的模型制定计划(流式,带 DeepMoney 备选)
yield self._format_sse("status", {"stage": "planning", "message": "正在制定执行计划..."})
messages = [
@@ -1603,9 +1640,9 @@ class MCPAgentIntegrated:
use_fallback = False
try:
# 尝试使用 Kimi 流式 API
stream = self.kimi_client.chat.completions.create(
model=self.kimi_model,
# 尝试使用选中的模型流式 API
stream = planning_client.chat.completions.create(
model=planning_model,
messages=messages,
temperature=1.0,
max_tokens=16000,
@@ -2165,11 +2202,12 @@ async def agent_chat_stream(chat_request: AgentChatRequest, request: Request):
except Exception as e:
logger.error(f"[ES] 保存用户消息失败: {e}")
# 获取工具列表
tools = [tool.dict() for tool in TOOLS]
# ==================== 动态工具过滤 ====================
# 获取所有可用工具
all_tools = [tool.dict() for tool in TOOLS]
# 添加特殊工具summarize_news
tools.append({
all_tools.append({
"name": "summarize_news",
"description": "使用 DeepMoney 模型总结新闻数据,提取关键信息",
"parameters": {
@@ -2188,6 +2226,21 @@ async def agent_chat_stream(chat_request: AgentChatRequest, request: Request):
}
})
# 如果用户指定了工具列表,则进行过滤
if chat_request.tools is not None and len(chat_request.tools) > 0:
selected_tool_names = set(chat_request.tools)
tools = [tool for tool in all_tools if tool["name"] in selected_tool_names]
logger.info(f"[工具过滤] 用户选择了 {len(tools)}/{len(all_tools)} 个工具: {selected_tool_names}")
else:
# 默认使用全部工具
tools = all_tools
logger.info(f"[工具过滤] 使用全部 {len(tools)} 个工具")
# ==================== 动态模型选择 ====================
selected_model = chat_request.model or "kimi-k2-thinking"
model_config = MODEL_CONFIGS.get(selected_model, MODEL_CONFIGS["kimi-k2-thinking"])
logger.info(f"[模型选择] 使用模型: {selected_model} ({model_config['model']})")
# 返回流式响应
return StreamingResponse(
agent.process_query_stream(
@@ -2199,6 +2252,7 @@ async def agent_chat_stream(chat_request: AgentChatRequest, request: Request):
user_nickname=chat_request.user_nickname,
user_avatar=chat_request.user_avatar,
cookies=cookies, # 传递 cookies 用于认证 API 调用
model_config=model_config, # 传递选中的模型配置
),
media_type="text/event-stream",
headers={

View File

@@ -5,7 +5,6 @@
"homepage": "/",
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@asseinfo/react-kanban": "^2.2.0",
"@chakra-ui/icons": "^2.2.6",
"@chakra-ui/react": "^2.10.9",
"@chakra-ui/theme-tools": "^2.2.6",
@@ -15,14 +14,21 @@
"@fontsource/open-sans": "^4.5.0",
"@fontsource/raleway": "^4.5.0",
"@fontsource/roboto": "^4.5.0",
"@fullcalendar/daygrid": "^5.9.0",
"@fullcalendar/interaction": "^5.9.0",
"@fullcalendar/react": "^5.9.0",
"@fullcalendar/core": "^6.1.19",
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/react": "^6.1.19",
"@reduxjs/toolkit": "^2.9.2",
"@splidejs/react-splide": "^0.7.12",
"@tanstack/react-virtual": "^3.13.12",
"@tippyjs/react": "^4.2.6",
"@visx/responsive": "^3.12.0",
"@visx/scale": "^3.12.0",
"@visx/text": "^3.12.0",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.0.0",
"@visx/visx": "^3.12.0",
"@visx/wordcloud": "^3.12.0",
"antd": "^5.27.4",
"apexcharts": "^3.27.3",
"axios": "^1.10.0",
@@ -34,60 +40,55 @@
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"echarts-wordcloud": "^2.1.0",
"framer-motion": "^4.1.17",
"framer-motion": "^12.23.24",
"fullcalendar": "^5.9.0",
"globalize": "^1.7.0",
"history": "^5.3.0",
"klinecharts": "^10.0.0-beta1",
"lucide-react": "^0.540.0",
"match-sorter": "6.3.0",
"nouislider": "15.0.0",
"posthog-js": "^1.295.0",
"react": "18.3.1",
"react": "^19.0.0",
"react-apexcharts": "^1.3.9",
"react-big-calendar": "^0.33.2",
"react-bootstrap-sweetalert": "5.2.0",
"react-circular-slider-svg": "^0.1.5",
"react-custom-scrollbars-2": "^4.4.0",
"react-datetime": "^3.0.4",
"react-dom": "^18.3.1",
"react-dropzone": "^11.4.2",
"react-dom": "^19.0.0",
"react-github-btn": "^1.2.1",
"react-icons": "^4.12.0",
"react-input-pin-code": "^1.1.5",
"react-just-parallax": "^3.1.16",
"react-jvectormap": "0.0.16",
"react-markdown": "^10.1.0",
"react-quill": "^2.0.0-beta.4",
"react-redux": "^9.2.0",
"react-responsive": "^10.0.1",
"react-responsive-masonry": "^2.7.1",
"react-router-dom": "^6.30.1",
"react-scripts": "^5.0.1",
"react-is": "^19.0.0",
"react-scroll": "^1.8.4",
"react-scroll-into-view": "^2.1.3",
"react-swipeable-views": "0.13.9",
"react-table": "^7.7.0",
"react-tagsinput": "3.19.0",
"react-to-print": "^2.13.0",
"react-tsparticles": "^2.12.2",
"react-wordcloud": "^1.2.7",
"react-to-print": "^3.0.3",
"recharts": "^3.1.2",
"sass": "^1.49.9",
"scroll-lock": "^2.1.5",
"socket.io-client": "^4.7.4",
"styled-components": "^5.3.11",
"stylis": "^4.0.10",
"stylis-plugin-rtl": "^2.1.1",
"tsparticles-slim": "^2.12.0",
"typescript": "^5.9.3"
},
"resolutions": {
"react-error-overlay": "6.0.9",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0"
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
},
"overrides": {
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"scripts": {
"prestart": "kill-port 3000",
@@ -100,7 +101,7 @@
"frontend:test": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.test craco start",
"dev": "npm start",
"backend": "python app.py",
"build": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.production craco build && gulp licenses",
"build": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' TSC_COMPILE_ON_ERROR=true DISABLE_ESLINT_PLUGIN=true env-cmd -f .env.production craco build && gulp licenses",
"build:analyze": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' ANALYZE=true craco build",
"test": "craco test --env=jsdom",
"eject": "react-scripts eject",
@@ -117,12 +118,11 @@
"devDependencies": {
"@craco/craco": "^7.1.0",
"@types/node": "^20.19.25",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.46.4",
"ajv": "^8.17.1",
"autoprefixer": "^10.4.21",
"concurrently": "^8.2.2",
"env-cmd": "^11.0.0",
"eslint-config-prettier": "8.3.0",
@@ -134,7 +134,6 @@
"imagemin-pngquant": "^10.0.0",
"kill-port": "^2.0.1",
"msw": "^2.11.5",
"postcss": "^8.5.6",
"prettier": "2.2.1",
"react-error-overlay": "6.0.9",
"sharp": "^0.34.4",

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
}

Binary file not shown.

BIN
public/LOGO_badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,736 @@
我将为您创建一个关于券商合并预期概念的现代化HTML页面融合金融专业性和视觉美感。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>券商合并预期 - 打造金融国家队</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.glass-effect {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.timeline-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(99, 102, 241, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
}
}
.stock-row:hover {
background: linear-gradient(90deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%);
transform: translateX(5px);
transition: all 0.3s ease;
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.timeline-line {
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 4px;
}
.badge-glow {
box-shadow: 0 0 10px rgba(99, 102, 241, 0.5);
}
</style>
</head>
<body>
<!-- Navigation -->
<div class="navbar glass-effect sticky top-0 z-50 px-4 py-3">
<div class="flex-1">
<a class="btn btn-ghost text-xl font-bold gradient-text">券商合并预期</a>
</div>
<div class="flex-none gap-2">
<button class="btn btn-ghost btn-circle">
<i class="fas fa-chart-line"></i>
</button>
<button class="btn btn-ghost btn-circle">
<i class="fas fa-bell"></i>
</button>
</div>
</div>
<!-- Hero Section -->
<div class="hero min-h-[60vh] glass-effect mx-4 mt-4 rounded-3xl" style="background-image: url('https://picsum.photos/seed/finance-merge/1920/800');">
<div class="hero-overlay bg-opacity-60 rounded-3xl"></div>
<div class="hero-content text-center text-white">
<div class="max-w-4xl">
<h1 class="mb-5 text-5xl font-bold animate-fade-in">
打造金融国家队
</h1>
<p class="mb-5 text-xl">
券商合并预期 - 从政策推动到价值重塑的三阶段演进
</p>
<div class="flex justify-center gap-4 mb-8">
<div class="stat glass-effect rounded-lg px-6 py-4">
<div class="stat-value text-2xl">2-3家</div>
<div class="stat-desc">2035年国际投行目标</div>
</div>
<div class="stat glass-effect rounded-lg px-6 py-4">
<div class="stat-value text-2xl">10家</div>
<div class="stat-desc">2025年优质机构目标</div>
</div>
<div class="stat glass-effect rounded-lg px-6 py-4">
<div class="stat-value text-2xl">50%↑</div>
<div class="stat-desc">CR5资产占比目标</div>
</div>
</div>
<button class="btn btn-primary btn-lg" onclick="scrollToSection('timeline')">
探索政策时间轴 <i class="fas fa-arrow-down ml-2"></i>
</button>
</div>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<!-- 政策时间轴 -->
<section id="timeline" class="mb-12">
<h2 class="text-3xl font-bold mb-8 text-white text-center">政策演进时间轴</h2>
<div class="timeline glass-effect rounded-2xl p-8">
<div class="relative">
<div class="timeline-line absolute left-8 top-0 bottom-0 w-1"></div>
<div class="mb-8 flex items-center">
<div class="timeline-dot w-4 h-4 bg-indigo-600 rounded-full absolute left-6"></div>
<div class="ml-16 glass-effect rounded-lg p-4 card-hover">
<div class="badge badge-primary badge-glow">2023年10月</div>
<h3 class="font-bold text-lg mt-2">中央金融工作会议</h3>
<p>首次提出"金融强国"目标,明确支持国有大型金融机构做优做强</p>
</div>
</div>
<div class="mb-8 flex items-center">
<div class="timeline-dot w-4 h-4 bg-purple-600 rounded-full absolute left-6"></div>
<div class="ml-16 glass-effect rounded-lg p-4 card-hover">
<div class="badge badge-secondary badge-glow">2024年3月</div>
<h3 class="font-bold text-lg mt-2">证监会指导意见</h3>
<p>明确5年内形成10家优质头部机构2035年形成2-3家国际投行</p>
</div>
</div>
<div class="mb-8 flex items-center">
<div class="timeline-dot w-4 h-4 bg-pink-600 rounded-full absolute left-6"></div>
<div class="ml-16 glass-effect rounded-lg p-4 card-hover">
<div class="badge badge-accent badge-glow">2024年4月</div>
<h3 class="font-bold text-lg mt-2">新"国九条"</h3>
<p>国务院层面首次支持投行通过并购重组提升核心竞争力</p>
</div>
</div>
<div class="mb-8 flex items-center">
<div class="timeline-dot w-4 h-4 bg-green-600 rounded-full absolute left-6"></div>
<div class="ml-16 glass-effect rounded-lg p-4 card-hover">
<div class="badge badge-success badge-glow">2024年9月</div>
<h3 class="font-bold text-lg mt-2">国泰君安+海通证券</h3>
<p>新"国九条"后首例头部券商合并总资产1.62万亿</p>
</div>
</div>
<div class="mb-8 flex items-center">
<div class="timeline-dot w-4 h-4 bg-yellow-600 rounded-full absolute left-6"></div>
<div class="ml-16 glass-effect rounded-lg p-4 card-hover">
<div class="badge badge-warning badge-glow">2025年11月</div>
<h3 class="font-bold text-lg mt-2">中金公司"一对二"合并</h3>
<p>一次性合并东兴、信达证券总资产达1万亿超出市场预期</p>
</div>
</div>
</div>
</div>
</section>
<!-- 核心逻辑与市场认知 -->
<section class="mb-12">
<h2 class="text-3xl font-bold mb-8 text-white text-center">核心逻辑与预期差</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="glass-effect rounded-2xl p-6 card-hover">
<div class="text-4xl mb-4">🏛️</div>
<h3 class="text-xl font-bold mb-3">政策强制力</h3>
<p class="text-gray-700">不同于以往市场化并购,本轮是"只许成功,不许失败"的国家战略,行政推动力度空前</p>
<div class="mt-4">
<div class="badge badge-info">顶层设计</div>
<div class="badge badge-error">硬性约束</div>
</div>
</div>
<div class="glass-effect rounded-2xl p-6 card-hover">
<div class="text-4xl mb-4">📉</div>
<h3 class="text-xl font-bold mb-3">行业生存压力</h3>
<p class="text-gray-700">经纪业务佣金率从8‱降至不足1‱IPO收紧、再融资停滞中小券商面临生存危机</p>
<div class="mt-4">
<div class="badge badge-warning">盈亏平衡</div>
<div class="badge badge-secondary">供给扭转</div>
</div>
</div>
<div class="glass-effect rounded-2xl p-6 card-hover">
<div class="text-4xl mb-4">💹</div>
<h3 class="text-xl font-bold mb-3">估值修复空间</h3>
<p class="text-gray-700">券商板块PB处于历史1.18%分位国泰君安PB仅0.88倍较中信存在100%修复空间</p>
<div class="mt-4">
<div class="badge badge-success">历史底部</div>
<div class="badge badge-primary">机构低配</div>
</div>
</div>
</div>
</section>
<!-- 关键催化剂 -->
<section class="mb-12">
<h2 class="text-3xl font-bold mb-8 text-white text-center">关键催化剂</h2>
<div class="glass-effect rounded-2xl p-8">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl p-6 text-white">
<i class="fas fa-calendar-alt text-3xl mb-4"></i>
<h3 class="text-xl font-bold mb-2">2025年12月-2026年1月</h3>
<p>国泰君安/海通合并后首份协同效应数据披露,验证"1+1"增量价值</p>
</div>
<div class="bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl p-6 text-white">
<i class="fas fa-chart-pie text-3xl mb-4"></i>
<h3 class="text-xl font-bold mb-2">2026年Q1</h3>
<p>中金公司吸收东兴、信达证券方案公布,汇金系整合路径明朗</p>
</div>
<div class="bg-gradient-to-r from-green-500 to-teal-500 rounded-xl p-6 text-white">
<i class="fas fa-landmark text-3xl mb-4"></i>
<h3 class="text-xl font-bold mb-2">2026年3月</h3>
<p>证监会《一流投行建设意见》中期评估,出台更具体鼓励政策</p>
</div>
<div class="bg-gradient-to-r from-orange-500 to-red-500 rounded-xl p-6 text-white">
<i class="fas fa-city text-3xl mb-4"></i>
<h3 class="text-xl font-bold mb-2">2026年Q2</h3>
<p>地方两会明确券商整合时间表,浙江系、深圳系后续动作</p>
</div>
</div>
</div>
</section>
<!-- 股票数据表格 -->
<section class="mb-12">
<h2 class="text-3xl font-bold mb-8 text-white text-center">券商股权结构全景图</h2>
<div class="glass-effect rounded-2xl p-6 overflow-x-auto">
<table class="table w-full">
<thead>
<tr class="bg-gradient-to-r from-indigo-500 to-purple-500 text-white">
<th>券商名称</th>
<th>分类</th>
<th>实际控制人/第一大股东</th>
<th>持股比例</th>
<th>消息来源</th>
<th>合并逻辑</th>
</tr>
</thead>
<tbody>
<tr class="stock-row">
<td class="font-bold">申万宏源</td>
<td><span class="badge badge-primary">汇金系</span></td>
<td>中央汇金投资有限责任公司</td>
<td>49.70%</td>
<td>年报</td>
<td class="text-sm">实际控制人中央汇金投资有限责任公司持股49.70%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">中国银河</td>
<td><span class="badge badge-primary">汇金系</span></td>
<td>中央汇金投资有限责任公司</td>
<td>32.76%</td>
<td>年报</td>
<td class="text-sm">实际控制人中央汇金投资有限责任公司持股32.76%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">东兴证券</td>
<td><span class="badge badge-secondary">财政系</span></td>
<td>国务院国资委</td>
<td>32.28%</td>
<td>年报</td>
<td class="text-sm">实际控制人国务院国资委持股32.28%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">信达证券</td>
<td><span class="badge badge-secondary">财政系</span></td>
<td>国务院</td>
<td>45.63%</td>
<td>年报</td>
<td class="text-sm">实际控制人国务院持股45.63%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">中信证券</td>
<td><span class="badge badge-info">中信系</span></td>
<td>中国中信金融控股有限公司</td>
<td>18.45%</td>
<td>年报</td>
<td class="text-sm">第一大股东中国中信金融控股有限公司持股18.45%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">中金财富证券</td>
<td><span class="badge badge-info">中信系</span></td>
<td>北京金融控股集团有限公司</td>
<td>35.81%</td>
<td>年报</td>
<td class="text-sm">第一大股东北京金融控股集团有限公司持股35.81%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">方正证券</td>
<td><span class="badge badge-warning">平安系</span></td>
<td>新方正控股发展有限责任公司</td>
<td>28.71%</td>
<td>年报</td>
<td class="text-sm">新方正控股发展有限责任公司持股28.71%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">平安证券</td>
<td><span class="badge badge-warning">平安系</span></td>
<td>-</td>
<td>-</td>
<td>年报</td>
<td class="text-sm">未上市</td>
</tr>
<tr class="stock-row">
<td class="font-bold">华安证券</td>
<td><span class="badge badge-success">安徽系</span></td>
<td>安徽省国资委</td>
<td>32.97%</td>
<td>年报</td>
<td class="text-sm">实际控制人安徽省国资委持股32.97%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">国元证券</td>
<td><span class="badge badge-success">安徽系</span></td>
<td>安徽省国资委</td>
<td>28.45%</td>
<td>年报</td>
<td class="text-sm">实际控制人安徽省国资委持股28.45%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">浙商证券</td>
<td><span class="badge badge-accent">浙江系</span></td>
<td>浙江交投投资集团</td>
<td>26.38%</td>
<td>年报</td>
<td class="text-sm">实际控制人浙江交投投资集团持股26.38%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">财通证券</td>
<td><span class="badge badge-accent">浙江系</span></td>
<td>浙江省财政厅</td>
<td>32.40%</td>
<td>年报</td>
<td class="text-sm">实际控制人浙江省财政厅持股32.40%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">华鑫股份</td>
<td><span class="badge badge-error">上海系</span></td>
<td>上海市国资委</td>
<td>55.26%</td>
<td>年报</td>
<td class="text-sm">实际控制人上海市国资委持股55.26%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">国泰君安</td>
<td><span class="badge badge-error">上海系</span></td>
<td>上海国际集团有限公司</td>
<td>18.83%</td>
<td>年报</td>
<td class="text-sm">实际控制人上海国际集团有限公司持股18.83%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">华泰证券</td>
<td><span class="badge badge-info">江苏系</span></td>
<td>江苏省国资委</td>
<td>28.59%</td>
<td>年报</td>
<td class="text-sm">实际控制人江苏省国资委持股28.59%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">东吴证券</td>
<td><span class="badge badge-info">江苏系</span></td>
<td>苏州市国资委</td>
<td>27.80%</td>
<td>年报</td>
<td class="text-sm">实际控制人苏州市国资委持股27.80%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">南京证券</td>
<td><span class="badge badge-info">江苏系</span></td>
<td>南京市国资委</td>
<td>28.90%</td>
<td>年报</td>
<td class="text-sm">实际控制人南京市国资委持股28.90%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">国联民生</td>
<td><span class="badge badge-info">江苏系</span></td>
<td>无锡市国资委</td>
<td>35.12%</td>
<td>年报</td>
<td class="text-sm">实际控制人无锡市国资委持股35.12%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">天风证券</td>
<td><span class="badge badge-warning">湖北系</span></td>
<td>湖北省财政厅</td>
<td>28.14%</td>
<td>年报</td>
<td class="text-sm">实际控制人湖北省财政厅持股28.14%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">长江证券</td>
<td><span class="badge badge-warning">湖北系</span></td>
<td>长江产业投资集团有限公司</td>
<td>17.41%</td>
<td>年报</td>
<td class="text-sm">第一大股东长江产业投资集团有限公司持股17.41%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">西南证券</td>
<td><span class="badge badge-secondary">川渝系</span></td>
<td>重庆市国资委</td>
<td>31.12%</td>
<td>年报</td>
<td class="text-sm">实际控制人重庆市国资委持股31.12%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">华西证券</td>
<td><span class="badge badge-secondary">川渝系</span></td>
<td>泸州市国资委</td>
<td>21.07%</td>
<td>年报</td>
<td class="text-sm">实际控制人泸州市国资委持股21.07%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">第一创业</td>
<td><span class="badge badge-primary">北京系</span></td>
<td>北京国有资本运营管理有限公司</td>
<td>11.06%</td>
<td>年报</td>
<td class="text-sm">第一大股东北京国有资本运营管理有限公司持股11.06%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">首创证券</td>
<td><span class="badge badge-primary">北京系</span></td>
<td>北京市国资委</td>
<td>82.39%</td>
<td>年报</td>
<td class="text-sm">实际控制人北京市国资委持股82.39%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">兴业证券</td>
<td><span class="badge badge-success">福建系</span></td>
<td>福建省财政厅</td>
<td>20.49%</td>
<td>年报</td>
<td class="text-sm">实际控制人福建省财政厅持股20.49%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">华福证券</td>
<td><span class="badge badge-success">福建系</span></td>
<td>-</td>
<td>-</td>
<td>年报</td>
<td class="text-sm">未上市</td>
</tr>
<tr class="stock-row">
<td class="font-bold">锦龙股份</td>
<td><span class="badge badge-accent">东莞系</span></td>
<td>杨志茂</td>
<td>18.80%</td>
<td>年报</td>
<td class="text-sm">实际控制人杨志茂持股18.80%</td>
</tr>
<tr class="stock-row">
<td class="font-bold">东莞证券</td>
<td><span class="badge badge-accent">东莞系</span></td>
<td>-</td>
<td>-</td>
<td>公开资料</td>
<td class="text-sm">2024年9月23日锦龙股份董事会通过转让东莞证券20%股份事项</td>
</tr>
<tr class="stock-row">
<td class="font-bold">中山证券</td>
<td><span class="badge badge-accent">东莞系</span></td>
<td>-</td>
<td>-</td>
<td>公开资料</td>
<td class="text-sm">锦龙股份持有中山证券67.78%股权</td>
</tr>
<tr class="stock-row">
<td class="font-bold">西部证券</td>
<td><span class="badge badge-error">最新推进</span></td>
<td>-</td>
<td>-</td>
<td>公开资料</td>
<td class="text-sm">西部证券在2025年9月成功控股国融证券</td>
</tr>
<tr class="stock-row">
<td class="font-bold">国信证券</td>
<td><span class="badge badge-error">最新推进</span></td>
<td>-</td>
<td>-</td>
<td>公开资料</td>
<td class="text-sm">2025年8月21日证监会核准国信证券成为万和证券主要股东</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- 投资建议 -->
<section class="mb-12">
<h2 class="text-3xl font-bold mb-8 text-white text-center">投资策略与风险提示</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="glass-effect rounded-2xl p-6">
<h3 class="text-2xl font-bold mb-4 gradient-text">最具投资价值方向</h3>
<div class="space-y-4">
<div class="alert alert-success">
<i class="fas fa-chart-line"></i>
<div>
<h4 class="font-bold">头部券商A+H股套利</h4>
<p class="text-sm">国泰君安H股、中信证券H股港股折价50%存在30-40%套利空间</p>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-shield-alt"></i>
<div>
<h4 class="font-bold">地方国资"唯一牌照"</h4>
<p class="text-sm">首创证券、东吴证券,地方国资做强动力,市值小并购期权未充分定价</p>
</div>
</div>
<div class="alert alert-warning">
<i class="fas fa-laptop-code"></i>
<div>
<h4 class="font-bold">金融IT真受益标的</h4>
<p class="text-sm">财富趋势、指南针C端流量平台合并后获客成本下降20%</p>
</div>
</div>
</div>
</div>
<div class="glass-effect rounded-2xl p-6">
<h3 class="text-2xl font-bold mb-4 gradient-text">核心跟踪指标</h3>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>指标类别</th>
<th>触发条件</th>
</tr>
</thead>
<tbody>
<tr>
<td>审批速度</td>
<td>受理到批复合计<90天</td>
</tr>
<tr>
<td>ROE提升</td>
<td>国泰君安+海通ROE>7.5%</td>
</tr>
<tr>
<td>市场情绪</td>
<td>成交额占比>5%</td>
</tr>
<tr>
<td>估值修复</td>
<td>PB回升至分位30%+</td>
</tr>
<tr>
<td>人才稳定</td>
<td>核心人员流失率<10%</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- 风险矩阵 -->
<section class="mb-12">
<h2 class="text-3xl font-bold mb-8 text-white text-center">风险矩阵</h2>
<div class="glass-effect rounded-2xl p-8">
<canvas id="riskChart" width="400" height="200"></canvas>
</div>
</section>
</div>
<!-- Footer -->
<footer class="glass-effect mt-12 py-8">
<div class="container mx-auto px-4 text-center">
<p class="text-gray-600">© 2024 券商合并预期分析报告 | 数据来源:公开资料整理</p>
<div class="mt-4 flex justify-center gap-4">
<a href="#" class="text-gray-400 hover:text-gray-600"><i class="fab fa-github text-2xl"></i></a>
<a href="#" class="text-gray-400 hover:text-gray-600"><i class="fab fa-twitter text-2xl"></i></a>
<a href="#" class="text-gray-400 hover:text-gray-600"><i class="fab fa-linkedin text-2xl"></i></a>
</div>
</div>
</footer>
<script>
// 平滑滚动
function scrollToSection(sectionId) {
document.getElementById(sectionId).scrollIntoView({ behavior: 'smooth' });
}
// 风险矩阵图表
const ctx = document.getElementById('riskChart').getContext('2d');
const riskChart = new Chart(ctx, {
type: 'bubble',
data: {
datasets: [{
label: '整合风险',
data: [{x: 5, y: 5, r: 25}],
backgroundColor: 'rgba(255, 99, 132, 0.6)'
}, {
label: '政策风险',
data: [{x: 4, y: 4, r: 20}],
backgroundColor: 'rgba(255, 159, 64, 0.6)'
}, {
label: '市场风险',
data: [{x: 3, y: 3, r: 15}],
backgroundColor: 'rgba(255, 205, 86, 0.6)'
}, {
label: '财务风险',
data: [{x: 2, y: 2, r: 10}],
backgroundColor: 'rgba(75, 192, 192, 0.6)'
}, {
label: '股东风险',
data: [{x: 1, y: 1, r: 8}],
backgroundColor: 'rgba(54, 162, 235, 0.6)'
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const impact = '影响程度: ' + context.parsed.y + '★';
const probability = '发生概率: ' + context.parsed.x + '★';
return [label, impact, probability];
}
}
}
},
scales: {
x: {
title: {
display: true,
text: '发生概率'
},
min: 0,
max: 6
},
y: {
title: {
display: true,
text: '影响程度'
},
min: 0,
max: 6
}
}
}
});
// 动画效果
document.addEventListener('DOMContentLoaded', function() {
const cards = document.querySelectorAll('.card-hover');
cards.forEach((card, index) => {
setTimeout(() => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
card.style.transition = 'all 0.5s ease';
setTimeout(() => {
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, 100);
}, index * 100);
});
});
</script>
</body>
</html>
这个HTML页面完整展现了券商合并预期概念的核心内容包括
1. **视觉设计**:采用渐变色背景、玻璃态效果、卡片悬浮动画等现代设计元素,营造专业金融科技感
2. **核心内容展示**
- Hero区域突出"打造金融国家队"主题
- 政策时间轴清晰展示演进过程
- 三层核心逻辑可视化呈现
- 关键催化剂时间表
- 完整的券商股权结构数据表格(响应式设计,支持横向滚动)
3. **交互功能**
- 平滑滚动导航
- 表格行悬停效果
- 风险矩阵气泡图
- 卡片动画效果
4. **数据呈现**
- 股票数据表格完整展示所有券商信息
- 使用徽章区分不同派系
- 颜色编码增强可读性
5. **投资价值**
- 明确的投资方向建议
- 核心跟踪指标
- 风险提示与应对策略
页面完全响应式设计,适配各种设备屏幕,同时保持了金融专业性和视觉美感的平衡。

View File

@@ -0,0 +1,971 @@
我将为您创建一个详实且炫酷的华为AI容器概念展示页面。这个页面将完整呈现Insight的深度内容并以现代化的设计风格展示相关股票数据。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>华为AI容器 - 算力效率革命的战略突围</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
* {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.timeline-line {
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
}
.hover-lift {
transition: all 0.3s ease;
}
.hover-lift:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.text-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.pulse-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
}
}
.stock-table {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.stock-table::-webkit-scrollbar {
height: 6px;
}
.stock-table::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.stock-table::-webkit-scrollbar-thumb {
background: #667eea;
border-radius: 10px;
}
.tag-cloud span {
transition: all 0.2s ease;
}
.tag-cloud span:hover {
transform: scale(1.05);
z-index: 10;
}
.metric-card {
transition: all 0.3s ease;
}
.metric-card:hover {
transform: scale(1.02);
}
</style>
</head>
<body class="bg-gray-50">
<!-- Hero Section -->
<div class="gradient-bg text-white">
<div class="container mx-auto px-4 py-16">
<div class="flex flex-col lg:flex-row items-center justify-between">
<div class="lg:w-2/3 mb-8 lg:mb-0">
<div class="flex items-center gap-2 mb-4">
<span class="bg-white/20 px-3 py-1 rounded-full text-sm">核心概念</span>
<span class="bg-red-500 text-white px-3 py-1 rounded-full text-sm flex items-center gap-1">
<i data-lucide="flame" class="w-4 h-4"></i>
舆情热度 20251120
</span>
</div>
<h1 class="text-4xl lg:text-6xl font-bold mb-4">华为AI容器</h1>
<p class="text-xl lg:text-2xl mb-6 opacity-90">算力效率革命的战略突围</p>
<p class="text-lg opacity-80 mb-8">在美国先进制程封锁倒逼下,通过系统级软件创新实现算力效率革命</p>
<div class="flex flex-wrap gap-3">
<div class="glass-effect px-4 py-2 rounded-lg">
<i data-lucide="calendar" class="inline w-5 h-5 mr-2"></i>
2025年11月21日发布
</div>
<div class="glass-effect px-4 py-2 rounded-lg">
<i data-lucide="code-2" class="inline w-5 h-5 mr-2"></i>
开源Flex:ai技术
</div>
<div class="glass-effect px-4 py-2 rounded-lg">
<i data-lucide="target" class="inline w-5 h-5 mr-2"></i>
对标英伟达AI技术
</div>
</div>
</div>
<div class="lg:w-1/3">
<div class="glass-effect rounded-2xl p-6">
<h3 class="text-xl font-semibold mb-4">核心催化剂</h3>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0 pulse-dot">
<span class="text-sm font-bold">1</span>
</div>
<div>
<p class="font-semibold">2025年9月</p>
<p class="text-sm opacity-80">UCM推理加速器开源</p>
</div>
</div>
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0">
<span class="text-sm font-bold">2</span>
</div>
<div>
<p class="font-semibold">2025年11月21日</p>
<p class="text-sm opacity-80">Flex:ai正式发布开源</p>
</div>
</div>
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center flex-shrink-0">
<span class="text-sm font-bold">3</span>
</div>
<div>
<p class="font-semibold">2027年Q4</p>
<p class="text-sm opacity-80">Atlas 960 SuperPoD发布</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Navigation Tabs -->
<div class="bg-white shadow-sm sticky top-0 z-40">
<div class="container mx-auto px-4">
<div class="tabs tabs-boxed flex flex-nowrap overflow-x-auto py-4" id="mainTabs">
<a href="#overview" class="tab tab-active" onclick="showSection('overview')">
<i data-lucide="eye" class="w-4 h-4 mr-2"></i>核心洞察
</a>
<a href="#logic" class="tab" onclick="showSection('logic')">
<i data-lucide="brain" class="w-4 h-4 mr-2"></i>核心逻辑
</a>
<a href="#catalyst" class="tab" onclick="showSection('catalyst')">
<i data-lucide="zap" class="w-4 h-4 mr-2"></i>催化剂
</a>
<a href="#industry" class="tab" onclick="showSection('industry')">
<i data-lucide="git-branch" class="w-4 h-4 mr-2"></i>产业链
</a>
<a href="#stocks" class="tab" onclick="showSection('stocks')">
<i data-lucide="line-chart" class="w-4 h-4 mr-2"></i>核心股票
</a>
<a href="#risk" class="tab" onclick="showSection('risk')">
<i data-lucide="alert-triangle" class="w-4 h-4 mr-2"></i>风险提示
</a>
</div>
</div>
</div>
<!-- Overview Section -->
<section id="overview" class="container mx-auto px-4 py-12">
<div class="grid lg:grid-cols-3 gap-6">
<!-- 核心观点 -->
<div class="lg:col-span-2">
<div class="bg-white rounded-2xl shadow-lg p-8 hover-lift">
<div class="flex items-center gap-3 mb-6">
<div class="w-12 h-12 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center">
<i data-lucide="lightbulb" class="w-6 h-6 text-white"></i>
</div>
<h2 class="text-2xl font-bold">核心观点摘要</h2>
</div>
<div class="prose prose-lg max-w-none">
<p class="text-gray-700 leading-relaxed mb-4">
华为AI容器概念正处于<strong class="text-gradient">"技术突破预期发酵期"</strong>,其核心驱动力并非单一产品发布,而是<strong class="text-gradient">在美国先进制程封锁倒逼下,通过系统级软件创新实现算力效率革命的战略突围</strong>
</p>
<p class="text-gray-700 leading-relaxed mb-4">
当前市场关注度逐步升温,但存在显著<strong class="text-purple-600">预期差</strong>:多数人聚焦昇腾硬件性能,却低估中间层容器技术作为"算力操作系统"的战略价值。
</p>
<p class="text-gray-700 leading-relaxed">
Flex:ai的开源若在性能上真正逼近英伟达生态将重塑国产AI算力价值链使华为从"卖铲子"升级为"制定采矿规则",具备<strong class="text-gradient">从主题投资向基本面驱动跃迁</strong>的潜力。
</p>
</div>
</div>
</div>
<!-- 市场热度评估 -->
<div>
<div class="bg-white rounded-2xl shadow-lg p-6 hover-lift">
<h3 class="text-xl font-bold mb-4 flex items-center">
<i data-lucide="trending-up" class="w-5 h-5 mr-2 text-purple-600"></i>
市场热度评估
</h3>
<div class="space-y-4">
<div>
<div class="flex justify-between mb-2">
<span class="text-sm text-gray-600">舆情热度</span>
<span class="text-sm font-semibold">80%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full" style="width: 80%"></div>
</div>
</div>
<div>
<div class="flex justify-between mb-2">
<span class="text-sm text-gray-600">研报覆盖</span>
<span class="text-sm font-semibold">30%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full" style="width: 30%"></div>
</div>
</div>
<div>
<div class="flex justify-between mb-2">
<span class="text-sm text-gray-600">交易拥挤度</span>
<span class="text-sm font-semibold">25%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full" style="width: 25%"></div>
</div>
</div>
</div>
<div class="mt-6 p-4 bg-purple-50 rounded-lg">
<p class="text-sm text-purple-700">
<i data-lucide="info" class="inline w-4 h-4 mr-1"></i>
距离11月发布尚有8个月属于<strong>预期发酵早期</strong>,尚未达到交易拥挤
</p>
</div>
</div>
</div>
</div>
<!-- 预期差分析 -->
<div class="mt-8 bg-white rounded-2xl shadow-lg p-8">
<h3 class="text-2xl font-bold mb-6 flex items-center">
<i data-lucide="layers" class="w-6 h-6 mr-3 text-purple-600"></i>
预期差深度挖掘
</h3>
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr class="text-left">
<th class="bg-purple-50 text-purple-700">市场普遍认知</th>
<th class="bg-purple-50 text-purple-700">被忽略的关键事实</th>
<th class="bg-purple-50 text-purple-700">预期差影响</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-gray-50 transition">
<td class="py-4">Flex:ai只是一个容器工具</td>
<td class="py-4">
<span class="inline-block px-2 py-1 bg-blue-100 text-blue-700 rounded text-sm mb-1">UCM9月开源</span>
<span class="inline-block px-2 py-1 bg-blue-100 text-blue-700 rounded text-sm">+ Flex:ai11月发布</span>
<p class="text-sm mt-1">构成"推理+训练"全栈优化</p>
</td>
<td class="py-4 text-green-600">技术价值被系统性低估</td>
</tr>
<tr class="hover:bg-gray-50 transition">
<td class="py-4">华为AI容器主要服务昇腾生态</td>
<td class="py-4">
<span class="inline-block px-2 py-1 bg-green-100 text-green-700 rounded text-sm">兼容K8s/Docker原生接口</span>
<p class="text-sm mt-1">可管理第三方云设施</p>
</td>
<td class="py-4 text-green-600">潜在市场空间从昇腾扩展到全行业</td>
</tr>
<tr class="hover:bg-gray-50 transition">
<td class="py-4">开源=不赚钱</td>
<td class="py-4">
<span class="inline-block px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-sm">博睿数据</span>
<span class="inline-block px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-sm">3000万采购订单</span>
<p class="text-sm mt-1">OEM模式已跑通</p>
</td>
<td class="py-4 text-green-600">商业模式已验证,变现路径清晰</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Logic Section -->
<section id="logic" class="container mx-auto px-4 py-12 hidden">
<div class="mb-8">
<h2 class="text-3xl font-bold mb-4">核心驱动力三维解析</h2>
<p class="text-gray-600">深入剖析华为AI容器背后的战略逻辑</p>
</div>
<div class="grid lg:grid-cols-3 gap-6">
<!-- 地缘政治驱动 -->
<div class="bg-white rounded-2xl shadow-lg overflow-hidden hover-lift">
<div class="bg-gradient-to-br from-red-500 to-orange-500 p-6 text-white">
<i data-lucide="shield-off" class="w-10 h-10 mb-3"></i>
<h3 class="text-xl font-bold">地缘政治驱动</h3>
<p class="text-sm opacity-90 mt-2">被迫创新下的突围</p>
</div>
<div class="p-6">
<div class="space-y-3">
<div class="flex items-start gap-3">
<i data-lucide="check-circle" class="w-5 h-5 text-green-500 mt-0.5"></i>
<div>
<p class="font-semibold">7nm制程限制</p>
<p class="text-sm text-gray-600">无法追赶英伟达最新GPU</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="check-circle" class="w-5 h-5 text-green-500 mt-0.5"></i>
<div>
<p class="font-semibold">软件定义硬件</p>
<p class="text-sm text-gray-600">系统级创新突破物理限制</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="check-circle" class="w-5 h-5 text-green-500 mt-0.5"></i>
<div>
<p class="font-semibold">CloudMatrix架构</p>
<p class="text-sm text-gray-600">384颗NPU全对等互联</p>
</div>
</div>
</div>
</div>
</div>
<!-- AI算力需求驱动 -->
<div class="bg-white rounded-2xl shadow-lg overflow-hidden hover-lift">
<div class="bg-gradient-to-br from-blue-500 to-cyan-500 p-6 text-white">
<i data-lucide="cpu" class="w-10 h-10 mb-3"></i>
<h3 class="text-xl font-bold">AI算力需求驱动</h3>
<p class="text-sm opacity-90 mt-2">场景驱动的爆发增长</p>
</div>
<div class="p-6">
<div class="space-y-3">
<div class="flex items-start gap-3">
<i data-lucide="trending-up" class="w-5 h-5 text-blue-500 mt-0.5"></i>
<div>
<p class="font-semibold">中国市场增速55%</p>
<p class="text-sm text-gray-600">AI服务器市场2023H1达30亿美元</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="trending-up" class="w-5 h-5 text-blue-500 mt-0.5"></i>
<div>
<p class="font-semibold">智能算力增速3-3.9倍</p>
<p class="text-sm text-gray-600">远超通用算力16.6%</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="trending-up" class="w-5 h-5 text-blue-500 mt-0.5"></i>
<div>
<p class="font-semibold">成本降低30%+</p>
<p class="text-sm text-gray-600">容器技术优化万卡集群效率</p>
</div>
</div>
</div>
</div>
</div>
<!-- 生态构建驱动 -->
<div class="bg-white rounded-2xl shadow-lg overflow-hidden hover-lift">
<div class="bg-gradient-to-br from-purple-500 to-indigo-500 p-6 text-white">
<i data-lucide="globe" class="w-10 h-10 mb-3"></i>
<h3 class="text-xl font-bold">生态构建驱动</h3>
<p class="text-sm opacity-90 mt-2">标准制定的战略布局</p>
</div>
<div class="p-6">
<div class="space-y-3">
<div class="flex items-start gap-3">
<i data-lucide="git-merge" class="w-5 h-5 text-purple-500 mt-0.5"></i>
<div>
<p class="font-semibold">开源复制TensorFlow路径</p>
<p class="text-sm text-gray-600">建立开发者生态</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="git-merge" class="w-5 h-5 text-purple-500 mt-0.5"></i>
<div>
<p class="font-semibold">20+万卡集群客户基础</p>
<p class="text-sm text-gray-600">锁定昇腾硬件采购</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="git-merge" class="w-5 h-5 text-purple-500 mt-0.5"></i>
<div>
<p class="font-semibold">软件-硬件-云服务闭环</p>
<p class="text-sm text-gray-600">类比CUDA锁定NV GPU</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Catalyst Section -->
<section id="catalyst" class="container mx-auto px-4 py-12 hidden">
<div class="mb-8">
<h2 class="text-3xl font-bold mb-4">关键催化剂与时间轴</h2>
<p class="text-gray-600">把握华为AI容器发展的关键节点</p>
</div>
<!-- Timeline -->
<div class="relative">
<div class="absolute left-1/2 transform -translate-x-1/2 w-1 h-full timeline-line"></div>
<!-- 2025年Q3 -->
<div class="relative flex items-center mb-8">
<div class="w-1/2 pr-8 text-right">
<div class="bg-white rounded-xl shadow-lg p-6 hover-lift">
<span class="inline-block px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm mb-2">近期催化剂</span>
<h3 class="text-xl font-bold mb-2">2025年9月 UCM正式开源</h3>
<p class="text-gray-600 text-sm">推理记忆数据管理器开源作为Flex:ai的前奏</p>
<div class="mt-3 flex justify-end gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">金融场景试点</span>
<span class="text-xs bg-gray-100 px-2 py-1 rounded">GitHub Star数</span>
</div>
</div>
</div>
<div class="absolute left-1/2 transform -translate-x-1/2 w-8 h-8 bg-blue-500 rounded-full border-4 border-white flex items-center justify-center">
<i data-lucide="calendar" class="w-4 h-4 text-white"></i>
</div>
<div class="w-1/2 pl-8"></div>
</div>
<!-- 2025年11月 -->
<div class="relative flex items-center mb-8">
<div class="w-1/2 pr-8"></div>
<div class="absolute left-1/2 transform -translate-x-1/2 w-8 h-8 bg-purple-500 rounded-full border-4 border-white flex items-center justify-center">
<i data-lucide="rocket" class="w-4 h-4 text-white"></i>
</div>
<div class="w-1/2 pl-8">
<div class="bg-white rounded-xl shadow-lg p-6 hover-lift">
<span class="inline-block px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm mb-2">核心事件</span>
<h3 class="text-xl font-bold mb-2">2025年11月21日 Flex:ai发布</h3>
<p class="text-gray-600 text-sm">上海论坛正式发布并开源对标英伟达AI技术</p>
<div class="mt-3 flex gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">性能测试数据</span>
<span class="text-xs bg-gray-100 px-2 py-1 rounded">社区活跃度</span>
</div>
</div>
</div>
</div>
<!-- 2026-2027 -->
<div class="relative flex items-center">
<div class="w-1/2 pr-8 text-right">
<div class="bg-white rounded-xl shadow-lg p-6 hover-lift">
<span class="inline-block px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm mb-2">长期发展</span>
<h3 class="text-xl font-bold mb-2">2026-2027 生态爆发期</h3>
<p class="text-gray-600 text-sm">Atlas 960 SuperPoD发布15,488张加速卡</p>
<div class="mt-3 flex justify-end gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">标准输出</span>
<span class="text-xs bg-gray-100 px-2 py-1 rounded">全球引领</span>
</div>
</div>
</div>
<div class="absolute left-1/2 transform -translate-x-1/2 w-8 h-8 bg-green-500 rounded-full border-4 border-white flex items-center justify-center">
<i data-lucide="target" class="w-4 h-4 text-white"></i>
</div>
<div class="w-1/2 pl-8"></div>
</div>
</div>
<!-- Key Metrics -->
<div class="grid lg:grid-cols-4 gap-4 mt-12">
<div class="bg-white rounded-xl shadow-lg p-6 text-center metric-card">
<div class="text-3xl font-bold text-gradient mb-2">3000万</div>
<p class="text-gray-600 text-sm">博睿数据订单金额</p>
</div>
<div class="bg-white rounded-xl shadow-lg p-6 text-center metric-card">
<div class="text-3xl font-bold text-gradient mb-2">20+</div>
<p class="text-gray-600 text-sm">CloudMatrix超级节点</p>
</div>
<div class="bg-white rounded-xl shadow-lg p-6 text-center metric-card">
<div class="text-3xl font-bold text-gradient mb-2">8192</div>
<p class="text-gray-600 text-sm">Atlas 950加速卡数</p>
</div>
<div class="bg-white rounded-xl shadow-lg p-6 text-center metric-card">
<div class="text-3xl font-bold text-gradient mb-2">245TB</div>
<p class="text-gray-600 text-sm">AI SSD单盘容量</p>
</div>
</div>
</section>
<!-- Industry Chain Section -->
<section id="industry" class="container mx-auto px-4 py-12 hidden">
<div class="mb-8">
<h2 class="text-3xl font-bold mb-4">产业链全景图</h2>
<p class="text-gray-600">华为AI容器的完整生态体系</p>
</div>
<div class="bg-white rounded-2xl shadow-lg p-8">
<!-- 上游:基础设施层 -->
<div class="mb-8">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 bg-gradient-to-br from-orange-500 to-red-500 rounded-lg flex items-center justify-center">
<i data-lucide="server" class="w-6 h-6 text-white"></i>
</div>
<h3 class="text-xl font-bold">上游:基础设施层</h3>
</div>
<div class="grid lg:grid-cols-4 gap-4">
<div class="bg-gray-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-orange-600">昇腾芯片</h4>
<p class="text-sm text-gray-600">昇腾910/950处理器<br>华为海思自主研发</p>
</div>
<div class="bg-gray-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-orange-600">CloudMatrix</h4>
<p class="text-sm text-gray-600">384超级节点架构<br>全对等互联技术</p>
</div>
<div class="bg-gray-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-orange-600">Atlas集群</h4>
<p class="text-sm text-gray-600">950/960 SuperPoD<br>万卡级算力集群</p>
</div>
<div class="bg-gray-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-orange-600">AI存储</h4>
<p class="text-sm text-gray-600">245TB AI SSD<br>"以存代算"技术</p>
</div>
</div>
</div>
<div class="border-t-2 border-dashed my-8"></div>
<!-- 中游AI容器平台层 -->
<div class="mb-8">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-500 rounded-lg flex items-center justify-center">
<i data-lucide="layers" class="w-6 h-6 text-white"></i>
</div>
<h3 class="text-xl font-bold">中游AI容器平台层</h3>
</div>
<div class="grid lg:grid-cols-4 gap-4">
<div class="bg-blue-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-blue-600">Flex:ai引擎</h4>
<p class="text-sm text-gray-600">开源AI容器引擎<br>核心调度算法</p>
</div>
<div class="bg-blue-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-blue-600">UCS管理平台</h4>
<p class="text-sm text-gray-600">华为云统一管理<br>分布式集群管控</p>
</div>
<div class="bg-blue-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-blue-600">UCM加速套件</h4>
<p class="text-sm text-gray-600">KV Cache优化<br>推理性能提升</p>
</div>
<div class="bg-blue-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-blue-600">中间件适配</h4>
<p class="text-sm text-gray-600">东方通/中国软件<br>信创生态整合</p>
</div>
</div>
</div>
<div class="border-t-2 border-dashed my-8"></div>
<!-- 下游:行业应用 -->
<div>
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-teal-500 rounded-lg flex items-center justify-center">
<i data-lucide="globe-2" class="w-6 h-6 text-white"></i>
</div>
<h3 class="text-xl font-bold">下游:行业应用与解决方案</h3>
</div>
<div class="grid lg:grid-cols-4 gap-4">
<div class="bg-green-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-green-600">智慧医疗</h4>
<p class="text-sm text-gray-600">DCS AI解决方案<br>华大系/塞力医疗</p>
</div>
<div class="bg-green-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-green-600">金融科技</h4>
<p class="text-sm text-gray-600">银联UCM试点<br>博睿数据运维</p>
</div>
<div class="bg-green-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-green-600">政务云</h4>
<p class="text-sm text-gray-600">混合云容器服务<br>电科数字/青云科技</p>
</div>
<div class="bg-green-50 rounded-lg p-4 hover:shadow-md transition">
<h4 class="font-semibold mb-2 text-green-600">企业服务</h4>
<p class="text-sm text-gray-600">企业级容器云<br>行业定制方案</p>
</div>
</div>
</div>
</div>
</section>
<!-- Stocks Section -->
<section id="stocks" class="container mx-auto px-4 py-12 hidden">
<div class="mb-8">
<h2 class="text-3xl font-bold mb-4">核心股票池</h2>
<p class="text-gray-600">华为AI容器产业链核心标的</p>
</div>
<!-- Stock Table -->
<div class="bg-white rounded-2xl shadow-lg overflow-hidden">
<div class="p-4 bg-gradient-to-r from-purple-600 to-indigo-600 text-white">
<h3 class="text-xl font-bold">股票数据详情</h3>
</div>
<div class="stock-table">
<table class="table w-full">
<thead>
<tr class="bg-gray-50">
<th class="text-left p-4">股票名称</th>
<th class="text-left p-4">分类</th>
<th class="text-left p-4">相关性描述</th>
<th class="text-left p-4">信源</th>
<th class="text-center p-4">强度</th>
</tr>
</thead>
<tbody>
<tr class="border-b hover:bg-purple-50 transition">
<td class="p-4 font-semibold">博睿数据</td>
<td class="p-4">
<span class="badge badge-info">技术服务</span>
</td>
<td class="p-4 text-sm">华为云UCS可观测软件框架供应商3000万采购订单已落地</td>
<td class="p-4 text-sm">互动</td>
<td class="p-4 text-center">
<div class="flex justify-center gap-1">
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
</div>
</td>
</tr>
<tr class="border-b hover:bg-purple-50 transition">
<td class="p-4 font-semibold">飞荣达</td>
<td class="p-4">
<span class="badge badge-secondary">散热</span>
</td>
<td class="p-4 text-sm">华为服务器液冷散热核心供应商受益Atlas集群放量</td>
<td class="p-4 text-sm">路演</td>
<td class="p-4 text-center">
<div class="flex justify-center gap-1">
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
</div>
</td>
</tr>
<tr class="border-b hover:bg-purple-50 transition">
<td class="p-4 font-semibold">青云科技</td>
<td class="p-4">
<span class="badge badge-primary">容器云</span>
</td>
<td class="p-4 text-sm">KubeSphere容器平台华为云原生生态伙伴</td>
<td class="p-4 text-sm">官网</td>
<td class="p-4 text-center">
<div class="flex justify-center gap-1">
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
</div>
</td>
</tr>
<tr class="border-b hover:bg-purple-50 transition">
<td class="p-4 font-semibold">神州数码</td>
<td class="p-4">
<span class="badge badge-primary">容器云</span>
</td>
<td class="p-4 text-sm">推出首个基于鲲鹏环境的开源容器云发行版</td>
<td class="p-4 text-sm">公告</td>
<td class="p-4 text-center">
<div class="flex justify-center gap-1">
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
</div>
</td>
</tr>
<tr class="border-b hover:bg-purple-50 transition">
<td class="p-4 font-semibold">电科数字</td>
<td class="p-4">
<span class="badge badge-primary">容器云</span>
</td>
<td class="p-4 text-sm">华讯容器云平台,华为云高级别合作伙伴</td>
<td class="p-4 text-sm">官微/互动</td>
<td class="p-4 text-center">
<div class="flex justify-center gap-1">
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
</div>
</td>
</tr>
<tr class="border-b hover:bg-purple-50 transition">
<td class="p-4 font-semibold">蜂助手</td>
<td class="p-4">
<span class="badge badge-success">技术服务</span>
</td>
<td class="p-4 text-sm">容器虚拟化核心技术,加强与华为产品级技术合作</td>
<td class="p-4 text-sm">互动</td>
<td class="p-4 text-center">
<div class="flex justify-center gap-1">
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
</div>
</td>
</tr>
<tr class="border-b hover:bg-purple-50 transition">
<td class="p-4 font-semibold">东方通</td>
<td class="p-4">
<span class="badge badge-warning">中间件</span>
</td>
<td class="p-4 text-sm">云原生中间件平台TongCNMP已应用于华为云</td>
<td class="p-4 text-sm">公告/互动</td>
<td class="p-4 text-center">
<div class="flex justify-center gap-1">
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-yellow-500 fill-current"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
<i data-lucide="star" class="w-4 h-4 text-gray-300"></i>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Investment Advice -->
<div class="mt-8 bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl p-8">
<h3 class="text-2xl font-bold mb-4">投资建议</h3>
<div class="grid lg:grid-cols-3 gap-6">
<div class="bg-white rounded-xl p-6">
<h4 class="font-semibold mb-3 text-green-600">首选</h4>
<p class="text-sm">博睿数据唯一明确获得华为云AI容器平台采购订单的公司OEM模式深度绑定收入确定性最高</p>
</div>
<div class="bg-white rounded-xl p-6">
<h4 class="font-semibold mb-3 text-blue-600">次选</h4>
<p class="text-sm">飞荣达算力效率提升必然带来功耗密度增加Atlas集群的万卡级部署对液冷需求爆发</p>
</div>
<div class="bg-white rounded-xl p-6">
<h4 class="font-semibold mb-3 text-yellow-600">备选</h4>
<p class="text-sm">华大系/塞力医疗华为医疗军团的DCS AI方案已明确集成容器技术需关注临床落地进展</p>
</div>
</div>
</div>
</section>
<!-- Risk Section -->
<section id="risk" class="container mx-auto px-4 py-12 hidden">
<div class="mb-8">
<h2 class="text-3xl font-bold mb-4">风险提示</h2>
<p class="text-gray-600">投资需关注的关键风险点</p>
</div>
<div class="grid lg:grid-cols-3 gap-6">
<!-- 技术风险 -->
<div class="bg-white rounded-2xl shadow-lg p-6 border-l-4 border-red-500">
<div class="flex items-center gap-3 mb-4">
<i data-lucide="alert-triangle" class="w-6 h-6 text-red-500"></i>
<h3 class="text-xl font-bold">技术风险</h3>
</div>
<ul class="space-y-2">
<li class="flex items-start gap-2">
<i data-lucide="chevron-right" class="w-4 h-4 text-red-500 mt-0.5"></i>
<span class="text-sm">Flex:ai性能能否真正对标英伟达存疑</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="chevron-right" class="w-4 h-4 text-red-500 mt-0.5"></i>
<span class="text-sm">开源社区活跃度不及预期</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="chevron-right" class="w-4 h-4 text-red-500 mt-0.5"></i>
<span class="text-sm">ISV生态构建速度缓慢</span>
</li>
</ul>
</div>
<!-- 商业化风险 -->
<div class="bg-white rounded-2xl shadow-lg p-6 border-l-4 border-orange-500">
<div class="flex items-center gap-3 mb-4">
<i data-lucide="trending-down" class="w-6 h-6 text-orange-500"></i>
<h3 class="text-xl font-bold">商业化风险</h3>
</div>
<ul class="space-y-2">
<li class="flex items-start gap-2">
<i data-lucide="chevron-right" class="w-4 h-4 text-orange-500 mt-0.5"></i>
<span class="text-sm">变现路径不清晰</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="chevron-right" class="w-4 h-4 text-orange-500 mt-0.5"></i>
<span class="text-sm">客户迁移成本高</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="chevron-right" class="w-4 h-4 text-orange-500 mt-0.5"></i>
<span class="text-sm">华为云容器收入占比低(<5%</span>
</li>
</ul>
</div>
<!-- 政策竞争风险 -->
<div class="bg-white rounded-2xl shadow-lg p-6 border-l-4 border-yellow-500">
<div class="flex items-center gap-3 mb-4">
<i data-lucide="shield" class="w-6 h-6 text-yellow-500"></i>
<h3 class="text-xl font-bold">政策与竞争风险</h3>
</div>
<ul class="space-y-2">
<li class="flex items-start gap-2">
<i data-lucide="chevron-right" class="w-4 h-4 text-yellow-500 mt-0.5"></i>
<span class="text-sm">阿里云、腾讯云快速跟进</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="chevron-right" class="w-4 h-4 text-yellow-500 mt-0.5"></i>
<span class="text-sm">国内云CR3市场份额超70%</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="chevron-right" class="w-4 h-4 text-yellow-500 mt-0.5"></i>
<span class="text-sm">开源软件出口管制风险</span>
</li>
</ul>
</div>
</div>
<!-- Risk Warning Box -->
<div class="mt-8 bg-red-50 border-2 border-red-200 rounded-xl p-6">
<div class="flex items-start gap-3">
<i data-lucide="alert-circle" class="w-6 h-6 text-red-500 mt-0.5"></i>
<div>
<h4 class="font-bold text-red-700 mb-2">重要提示</h4>
<p class="text-sm text-red-600">
距离11月Flex:ai发布尚有8个月时间窗口存在重大不确定性。历史上华为曾延期发布产品需警惕"预期过度前置"风险。
建议在9月UCM开源后根据社区热度动态调整仓位若技术反馈正面可加仓若平淡则减仓锁定收益。
</p>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-gray-900 text-white py-12 mt-20">
<div class="container mx-auto px-4">
<div class="text-center">
<p class="text-sm opacity-75">© 2025 华为AI容器概念分析 | 数据来源:公开信息整理</p>
<p class="text-xs opacity-50 mt-2">投资有风险,入市需谨慎</p>
</div>
</div>
</footer>
<script>
// Initialize Lucide icons
lucide.createIcons();
// Tab navigation
function showSection(sectionId) {
// Hide all sections
const sections = document.querySelectorAll('section');
sections.forEach(section => {
section.classList.add('hidden');
});
// Show selected section
const selectedSection = document.getElementById(sectionId);
if (selectedSection) {
selectedSection.classList.remove('hidden');
}
// Update tab active state
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.classList.remove('tab-active');
});
event.target.classList.add('tab-active');
// Reinitialize icons for newly shown content
setTimeout(() => lucide.createIcons(), 100);
}
// Add smooth scroll
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
// Add animation on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
// Observe all metric cards and hover-lift elements
document.querySelectorAll('.metric-card, .hover-lift').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'all 0.6s ease';
observer.observe(el);
});
</script>
</body>
</html>
这个HTML页面完整展示了华为AI容器概念的深度分析主要特点包括
## 核心功能:
1. **详实的内容保留** - 完整呈现了Insight中的核心观点、逻辑分析、催化剂时间轴等
2. **炫酷的视觉设计** - 使用渐变色、玻璃态效果、动画过渡等现代设计元素
3. **股票数据表格** - 清晰展示相关股票及其关联度,使用星级评价系统
4. **响应式布局** - 完美适配移动端和桌面端
5. **交互式导航** - 标签页切换不同内容模块
6. **数据可视化** - 进度条、时间轴、卡片等多种数据展示方式
## 设计亮点:
- 渐变色主题贯穿全站
- 卡片悬浮效果增强交互感
- 时间轴清晰展示发展路径
- 星级评价直观展示股票关联度
- 风险提示醒目突出
- 移动端友好的触控交互
页面内容深度与视觉效果并重,既保留了专业金融分析的严谨性,又具有现代化的审美体验。

615
public/htmls/寒潮.html Normal file
View File

@@ -0,0 +1,615 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>寒潮投资概念深度解析 - 季节性事件驱动的短期机遇</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.0/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.glass-morphism {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.hero-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.pulse-animation {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.timeline-item::before {
content: '';
position: absolute;
left: -8px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
border-radius: 50%;
background: #667eea;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.2);
}
.timeline-line {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(180deg, #667eea, #764ba2);
}
.stock-table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
@media (max-width: 768px) {
.stock-table {
font-size: 0.75rem;
}
}
.floating-icon {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.gradient-text {
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>
</head>
<body class="bg-gray-50">
<!-- Hero Section -->
<div class="hero-gradient text-white py-20">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row items-center justify-between">
<div class="md:w-2/3 mb-10 md:mb-0">
<div class="flex items-center mb-4">
<i class="fas fa-snowflake text-4xl mr-4 floating-icon"></i>
<h1 class="text-5xl font-bold">寒潮投资概念</h1>
</div>
<p class="text-xl mb-6">季节性事件驱动的短期机遇分析</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="bg-white/20 backdrop-blur rounded-lg p-4">
<div class="text-3xl font-bold">15-30</div>
<div class="text-sm">有效窗口期(天)</div>
</div>
<div class="bg-white/20 backdrop-blur rounded-lg p-4">
<div class="text-3xl font-bold">3次</div>
<div class="text-sm">2024-25预警峰值</div>
</div>
<div class="bg-white/20 backdrop-blur rounded-lg p-4">
<div class="text-3xl font-bold">10%</div>
<div class="text-sm">波司登单日涨幅</div>
</div>
<div class="bg-white/20 backdrop-blur rounded-lg p-4">
<div class="text-3xl font-bold">60万吨</div>
<div class="text-sm">东三省日耗峰值</div>
</div>
</div>
</div>
<div class="md:w-1/3">
<div class="bg-white/10 backdrop-blur rounded-2xl p-6">
<h3 class="text-xl font-semibold mb-4">当前阶段判断</h3>
<div class="space-y-3">
<div class="flex items-center">
<i class="fas fa-fire text-yellow-400 mr-3"></i>
<span>季节性事件驱动高峰期</span>
</div>
<div class="flex items-center">
<i class="fas fa-chart-line text-green-400 mr-3"></i>
<span>短期主题投资机会</span>
</div>
<div class="flex items-center">
<i class="fas fa-exclamation-triangle text-orange-400 mr-3"></i>
<span>非长期产业趋势</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="container mx-auto px-4 py-10">
<!-- 时间轴 -->
<div class="glass-morphism rounded-2xl p-8 mb-8">
<h2 class="text-3xl font-bold gradient-text mb-6">概念事件时间轴</h2>
<div class="relative">
<div class="timeline-line"></div>
<div class="space-y-6">
<div class="timeline-item relative pl-8">
<div class="bg-blue-50 rounded-lg p-4">
<div class="font-semibold text-blue-600">2024年11月下旬</div>
<div>首轮预警启动寒潮蓝色预警0℃线南压至苏皖北部</div>
</div>
</div>
<div class="timeline-item relative pl-8">
<div class="bg-orange-50 rounded-lg p-4">
<div class="font-semibold text-orange-600">2025年1月23日</div>
<div>中国气象局启动寒潮暴雪Ⅲ级应急响应局地降温超20℃</div>
</div>
</div>
<div class="timeline-item relative pl-8">
<div class="bg-purple-50 rounded-lg p-4">
<div class="font-semibold text-purple-600">2025年1-2月</div>
<div>欧美同步寒潮德国天然气消费激增TTF价格突破50美元</div>
</div>
</div>
<div class="timeline-item relative pl-8">
<div class="bg-green-50 rounded-lg p-4">
<div class="font-semibold text-green-600">2025年3月</div>
<div>寒潮延续至春季北京发布蓝色预警0℃线南压至秦岭</div>
</div>
</div>
</div>
</div>
</div>
<!-- 核心逻辑 -->
<div class="grid md:grid-cols-3 gap-6 mb-8">
<div class="glass-morphism rounded-xl p-6 card-hover">
<div class="text-blue-500 text-4xl mb-4">
<i class="fas fa-cloud-showers-heavy"></i>
</div>
<h3 class="text-xl font-bold mb-3">气象灾害冲击</h3>
<p class="text-gray-600">全国大部降温6-10℃局地超20℃官方定调"极端天气事件"</p>
<div class="mt-4 pt-4 border-t">
<span class="text-sm text-gray-500">东三省日耗从35万→60万吨/天</span>
</div>
</div>
<div class="glass-morphism rounded-xl p-6 card-hover">
<div class="text-purple-500 text-4xl mb-4">
<i class="fas fa-link"></i>
</div>
<h3 class="text-xl font-bold mb-3">供应链脆弱性</h3>
<p class="text-gray-600">上游原料紧张、中游订单爆单、下游需求即时</p>
<div class="mt-4 pt-4 border-t">
<span class="text-sm text-gray-500">古麒绒材羽绒收入占比99.27%</span>
</div>
</div>
<div class="glass-morphism rounded-xl p-6 card-hover">
<div class="text-green-500 text-4xl mb-4">
<i class="fas fa-bolt"></i>
</div>
<h3 class="text-xl font-bold mb-3">能源刚性需求</h3>
<p class="text-gray-600">空调制热+煤炭/天然气供暖叠加,需求非线性增长</p>
<div class="mt-4 pt-4 border-t">
<span class="text-sm text-gray-500">提前10天供暖多耗5000万吨煤</span>
</div>
</div>
</div>
<!-- 产业链分析 -->
<div class="glass-morphism rounded-2xl p-8 mb-8">
<h2 class="text-3xl font-bold gradient-text mb-6">产业链四象限模型</h2>
<div class="grid md:grid-cols-2 gap-6">
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-6">
<h3 class="text-lg font-bold text-blue-700 mb-4">能源链(需求强度:★★★★★)</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>上游资源:</span>
<span class="font-semibold">中国神华、陕西煤业</span>
</div>
<div class="flex justify-between">
<span>中游制造:</span>
<span class="font-semibold">古麒绒材</span>
</div>
<div class="flex justify-between">
<span>下游消费:</span>
<span class="font-semibold">波司登</span>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl p-6">
<h3 class="text-lg font-bold text-green-700 mb-4">设备链(需求强度:★★★★)</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>核心原料:</span>
<span class="font-semibold">棉花/鸭绒</span>
</div>
<div class="flex justify-between">
<span>热泵设备:</span>
<span class="font-semibold">日出东方、双良节能</span>
</div>
<div class="flex justify-between">
<span>终端零售:</span>
<span class="font-semibold">彩虹集团</span>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl p-6">
<h3 class="text-lg font-bold text-purple-700 mb-4">服务链(需求强度:★★★)</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>电力供应:</span>
<span class="font-semibold">华能、华电</span>
</div>
<div class="flex justify-between">
<span>智能温控:</span>
<span class="font-semibold">瑞纳智能</span>
</div>
<div class="flex justify-between">
<span>热力服务:</span>
<span class="font-semibold">联美控股</span>
</div>
</div>
</div>
<div class="bg-gradient-to-br from-orange-50 to-yellow-50 rounded-xl p-6">
<h3 class="text-lg font-bold text-orange-700 mb-4">消费品链(需求强度:★★)</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>品牌服装:</span>
<span class="font-semibold">波司登、太平鸟</span>
</div>
<div class="flex justify-between">
<span>家居保暖:</span>
<span class="font-semibold">真爱美家</span>
</div>
<div class="flex justify-between">
<span>小家电:</span>
<span class="font-semibold">彩虹集团</span>
</div>
</div>
</div>
</div>
</div>
<!-- 股票数据表格 -->
<div class="glass-morphism rounded-2xl p-8 mb-8">
<h2 class="text-3xl font-bold gradient-text mb-6">相关上市公司一览</h2>
<!-- 供热/暖板块 -->
<div class="mb-8">
<h3 class="text-xl font-semibold mb-4 text-blue-600">
<i class="fas fa-fire mr-2"></i>供热/暖板块
</h3>
<div class="stock-table">
<table class="table table-zebra table-compact w-full">
<thead>
<tr class="bg-blue-100">
<th>股票代码</th>
<th>公司名称</th>
<th>相关性描述</th>
<th>核心逻辑</th>
</tr>
</thead>
<tbody>
<tr>
<td>-</td>
<td>联美控股</td>
<td>2025H1供暖及蒸汽收入14.16亿元占比74.62%</td>
<td>供暖收入占比高,直接受益寒潮</td>
</tr>
<tr>
<td>-</td>
<td>惠天热电</td>
<td>沈阳市大型专业供热上市公司2025H1供暖供热气11.66亿元占比96.35%</td>
<td>专业供热且收入占比极高</td>
</tr>
<tr>
<td>-</td>
<td>京能热力</td>
<td>实控人北京市国资委持股28.46%2025H1热力服务收入6.55亿元占比84.67%</td>
<td>国资控股保障,区域垄断性强</td>
</tr>
<tr>
<td>-</td>
<td>金房能源</td>
<td>2025H1供热行业6.55亿元占比97.59%北京地区占比62.11%</td>
<td>供热收入占比极高,区域集中</td>
</tr>
<tr>
<td>-</td>
<td>瑞纳智能</td>
<td>2025H1供热节能服务0.74亿元占比79.59%</td>
<td>智慧供暖,技术壁垒高</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 热泵板块 -->
<div class="mb-8">
<h3 class="text-xl font-semibold mb-4 text-green-600">
<i class="fas fa-temperature-high mr-2"></i>热泵板块
</h3>
<div class="stock-table">
<table class="table table-zebra table-compact w-full">
<thead>
<tr class="bg-green-100">
<th>股票代码</th>
<th>公司名称</th>
<th>相关性描述</th>
<th>核心逻辑</th>
</tr>
</thead>
<tbody>
<tr>
<td>-</td>
<td>日出东方</td>
<td>推进以空气能热泵为龙头业务的战略布局空气能热泵产能10万套左右</td>
<td>热泵为核心业务西藏项目17.67亿</td>
</tr>
<tr>
<td>-</td>
<td>万和电气</td>
<td>2024年推出的R290热泵产品成为行业内的佼佼者</td>
<td>R290热泵技术领先</td>
</tr>
<tr>
<td>-</td>
<td>双良节能</td>
<td>中国最大的溴化锂制冷机、吸收式热泵和空冷器生产商和集成商</td>
<td>溴化锂热泵领域龙头</td>
</tr>
<tr>
<td>-</td>
<td>振邦智能</td>
<td>空气能热泵是公司主要产品智能电器控制器的主要应用之一</td>
<td>热泵控制器核心供应商</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 御寒产品板块 -->
<div class="mb-8">
<h3 class="text-xl font-semibold mb-4 text-purple-600">
<i class="fas fa-tshirt mr-2"></i>御寒产品板块
</h3>
<div class="stock-table">
<table class="table table-zebra table-compact w-full">
<thead>
<tr class="bg-purple-100">
<th>股票代码</th>
<th>公司名称</th>
<th>相关性描述</th>
<th>核心逻辑</th>
</tr>
</thead>
<tbody>
<tr>
<td>-</td>
<td>古麒绒材</td>
<td>主要产品为高规格羽绒材料2025H1羽绒行业5.33亿元占比99.27%</td>
<td>最纯粹的羽绒原料标的</td>
</tr>
<tr>
<td>-</td>
<td>华英农业</td>
<td>2025H1羽绒13.55亿元占比65.43%</td>
<td>羽绒收入占比较高</td>
</tr>
<tr>
<td>-</td>
<td>真爱美家</td>
<td>2025H1毛毯3.45亿元占比88%,保暖性和舒适度高</td>
<td>毛毯收入占比极高</td>
</tr>
<tr>
<td>-</td>
<td>彩虹集团</td>
<td>电热毯行业龙头2025H1家用柔性取暖系列3.56亿元占比65.73%</td>
<td>电热毯龙头,需求反馈最快</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 能源板块 -->
<div class="mb-8">
<h3 class="text-xl font-semibold mb-4 text-orange-600">
<i class="fas fa-oil-can mr-2"></i>能源板块
</h3>
<div class="stock-table">
<table class="table table-zebra table-compact w-full">
<thead>
<tr class="bg-orange-100">
<th>股票代码</th>
<th>公司名称</th>
<th>相关性描述</th>
<th>核心逻辑</th>
</tr>
</thead>
<tbody>
<tr>
<td>601088</td>
<td>中国神华</td>
<td>煤炭产业链龙头股息率4.8%</td>
<td>长协煤保供能力强,现金流稳定</td>
</tr>
<tr>
<td>601225</td>
<td>陕西煤业</td>
<td>煤炭产业链股息率4.5%</td>
<td>资源禀赋优秀,成本优势明显</td>
</tr>
<tr>
<td>600188</td>
<td>兖矿能源</td>
<td>煤炭产业链,澳洲产能占比高</td>
<td>受益国际煤价上涨,弹性大</td>
</tr>
<tr>
<td>601018</td>
<td>中煤能源</td>
<td>煤炭产业链,央企背景</td>
<td>产能规模大,保供主力</td>
</tr>
<tr>
<td>600997</td>
<td>开滦股份</td>
<td>焦炭产业链,煤焦一体化</td>
<td>焦煤涨价受益,弹性较好</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 投资策略 -->
<div class="grid md:grid-cols-2 gap-6 mb-8">
<div class="glass-morphism rounded-xl p-6">
<h3 class="text-xl font-bold mb-4 text-green-600">
<i class="fas fa-chess-rook mr-2"></i>操作策略
</h3>
<div class="space-y-4">
<div class="bg-green-50 rounded-lg p-4">
<div class="font-semibold mb-2">三日法则</div>
<p class="text-sm text-gray-600">每3日评估一次若数据未达预期立即减半</p>
</div>
<div class="bg-yellow-50 rounded-lg p-4">
<div class="font-semibold mb-2">止损纪律</div>
<p class="text-sm text-gray-600">买入3日内若下跌超5%,立即止损</p>
</div>
<div class="bg-blue-50 rounded-lg p-4">
<div class="font-semibold mb-2">仓位控制</div>
<p class="text-sm text-gray-600">主题配置不超过10%,卫星策略定位</p>
</div>
</div>
</div>
<div class="glass-morphism rounded-xl p-6">
<h3 class="text-xl font-bold mb-4 text-red-600">
<i class="fas fa-exclamation-triangle mr-2"></i>关键风险
</h3>
<div class="space-y-4">
<div class="bg-red-50 rounded-lg p-4">
<div class="font-semibold mb-2">可持续性风险</div>
<p class="text-sm text-gray-600">天气红利难持续,寒潮结束需求断崖</p>
</div>
<div class="bg-orange-50 rounded-lg p-4">
<div class="font-semibold mb-2">技术风险</div>
<p class="text-sm text-gray-600">热泵低温性能衰减,-10℃以下COP降至2.5</p>
</div>
<div class="bg-purple-50 rounded-lg p-4">
<div class="font-semibold mb-2">政策风险</div>
<p class="text-sm text-gray-600">保供压制煤价长协上限770元/吨</p>
</div>
</div>
</div>
</div>
<!-- 关键跟踪指标 -->
<div class="glass-morphism rounded-2xl p-8">
<h2 class="text-3xl font-bold gradient-text mb-6">关键跟踪指标</h2>
<div class="grid md:grid-cols-3 gap-6">
<div class="bg-gradient-to-r from-red-500 to-orange-500 text-white rounded-xl p-6">
<div class="text-2xl font-bold mb-2">一级指标(每日)</div>
<ul class="space-y-2 text-sm">
<li>• 中央气象台预警等级</li>
<li>• 东三省电厂日耗(>60万吨</li>
<li>• 生猪日屠宰量(>17万头</li>
</ul>
</div>
<div class="bg-gradient-to-r from-blue-500 to-indigo-500 text-white rounded-xl p-6">
<div class="text-2xl font-bold mb-2">二级指标(每周)</div>
<ul class="space-y-2 text-sm">
<li>• 秦皇岛港库存(<500万吨</li>
<li>• 羽绒原料价格指数(周涨>5%</li>
<li>• 小家电换手率(>5%</li>
</ul>
</div>
<div class="bg-gradient-to-r from-green-500 to-teal-500 text-white rounded-xl p-6">
<div class="text-2xl font-bold mb-2">三级指标(每月)</div>
<ul class="space-y-2 text-sm">
<li>• 天猫京东羽绒服销量</li>
<li>• 企业月度经营数据</li>
<li>• 能源库存变动</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="hero-gradient text-white py-8 mt-20">
<div class="container mx-auto px-4 text-center">
<p class="mb-2">寒潮投资概念解析 - 数据更新至2025年</p>
<p class="text-sm opacity-75">本内容仅供参考,不构成投资建议</p>
</div>
</footer>
<script>
// 动态效果
document.addEventListener('DOMContentLoaded', function() {
// 滚动动画
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -100px 0px'
};
const observer = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
document.querySelectorAll('.card-hover').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'all 0.6s ease';
observer.observe(el);
});
// 表格行悬停效果
document.querySelectorAll('tbody tr').forEach(row => {
row.addEventListener('mouseenter', function() {
this.style.transform = 'scale(1.02)';
this.style.transition = 'all 0.2s ease';
});
row.addEventListener('mouseleave', function() {
this.style.transform = 'scale(1)';
});
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,746 @@
我将为您创建一个专业的金融投资分析页面,展示"对日反制"投资概念的深度分析。这个页面将融合金融严谨性与现代设计美学。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>对日反制投资概念深度分析</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
* {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.glass-morphism {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.gradient-border {
position: relative;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2px;
border-radius: 1rem;
}
.gradient-border-inner {
background: #0f172a;
border-radius: calc(1rem - 2px);
padding: 1.5rem;
}
.timeline-line {
background: linear-gradient(180deg, #3b82f6 0%, #8b5cf6 100%);
}
.hover-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.hover-card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
}
.pulse-dot {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; }
}
.stock-table {
overflow-x: auto;
}
.stock-table::-webkit-scrollbar {
height: 8px;
}
.stock-table::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.stock-table::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
border-radius: 4px;
}
.tag-primary {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
}
.tag-secondary {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
}
.tag-accent {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
}
.number-stat {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.risk-high { border-left: 4px solid #ef4444; }
.risk-medium { border-left: 4px solid #f59e0b; }
.risk-low { border-left: 4px solid #10b981; }
</style>
</head>
<body class="bg-gray-950 text-white">
<!-- Hero Section -->
<div class="relative min-h-screen flex items-center justify-center overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-blue-900/20 via-purple-900/20 to-gray-950"></div>
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%239C92AC" fill-opacity="0.05"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')]"></div>
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div class="text-center mb-12">
<div class="inline-flex items-center px-4 py-2 rounded-full bg-gradient-to-r from-blue-500/20 to-purple-500/20 backdrop-blur-sm mb-6">
<span class="pulse-dot w-2 h-2 bg-green-400 rounded-full mr-2"></span>
<span class="text-sm font-medium text-gray-300">2025年下半年地缘政治驱动的投资机会</span>
</div>
<h1 class="text-5xl md:text-7xl font-bold mb-6 bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
对日反制概念
</h1>
<p class="text-xl text-gray-400 max-w-3xl mx-auto leading-relaxed">
地缘政治博弈下的国产替代加速,从精准制裁到产业链重构的投资逻辑梳理
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16">
<div class="glass-morphism rounded-2xl p-6 hover-card">
<div class="flex items-center mb-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-r from-red-500 to-orange-500 flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-white text-xl"></i>
</div>
<h3 class="ml-4 text-lg font-semibold">风险警示</h3>
</div>
<p class="text-gray-400">主题炒作早期阶段,警惕情绪退潮风险,政策不确定性高</p>
</div>
<div class="glass-morphism rounded-2xl p-6 hover-card">
<div class="flex items-center mb-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-r from-blue-500 to-cyan-500 flex items-center justify-center">
<i class="fas fa-industry text-white text-xl"></i>
</div>
<h3 class="ml-4 text-lg font-semibold">核心机会</h3>
</div>
<p class="text-gray-400">资源管制 > 材料替代 > 水产出口,关注实质性国产替代标的</p>
</div>
<div class="glass-morphism rounded-2xl p-6 hover-card">
<div class="flex items-center mb-4">
<div class="w-12 h-12 rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center">
<i class="fas fa-chart-line text-white text-xl"></i>
</div>
<h3 class="ml-4 text-lg font-semibold">时间窗口</h3>
</div>
<p class="text-gray-400">3-6个月关键观察期等待政策验证不赌情绪溢价</p>
</div>
</div>
</div>
</div>
<!-- Timeline Section -->
<div class="py-20 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold mb-12 text-center">
<span class="bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
关键事件时间轴
</span>
</h2>
<div class="relative">
<div class="absolute left-1/2 transform -translate-x-1/2 w-1 h-full timeline-line rounded-full"></div>
<div class="space-y-12">
<!-- Event 1 -->
<div class="flex items-center justify-center">
<div class="w-full md:w-5/12 text-right pr-8 hidden md:block">
<div class="gradient-border">
<div class="gradient-border-inner">
<div class="text-sm text-purple-400 mb-2">2025年5月19日</div>
<h4 class="text-lg font-semibold mb-2">背景伏笔:资源管制启动</h4>
<p class="text-gray-400 text-sm">缅甸成为日本T金属第一大供应国但货源依赖中国境内非法走私中方加强全链条管控</p>
</div>
</div>
</div>
<div class="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 bg-blue-500 rounded-full border-4 border-gray-950"></div>
<div class="w-full md:w-5/12 pl-8 md:hidden">
<div class="gradient-border">
<div class="gradient-border-inner">
<div class="text-sm text-purple-400 mb-2">2025年5月19日</div>
<h4 class="text-lg font-semibold mb-2">背景伏笔:资源管制启动</h4>
<p class="text-gray-400 text-sm">缅甸成为日本T金属第一大供应国但货源依赖中国境内非法走私中方加强全链条管控</p>
</div>
</div>
</div>
</div>
<!-- Event 2 -->
<div class="flex items-center justify-center">
<div class="w-full md:w-5/12 text-right pr-8 hidden md:block">
<div class="gradient-border">
<div class="gradient-border-inner">
<div class="text-sm text-orange-400 mb-2">2025年9月8日</div>
<h4 class="text-lg font-semibold mb-2">精准制裁落地</h4>
<p class="text-gray-400 text-sm">外交部依据《反外国制裁法》对日本参议员石平实施制裁,冻结资产、禁止交易、人员禁入</p>
</div>
</div>
</div>
<div class="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 bg-orange-500 rounded-full border-4 border-gray-950 pulse-dot"></div>
<div class="w-full md:w-5/12 pl-8 md:hidden">
<div class="gradient-border">
<div class="gradient-border-inner">
<div class="text-sm text-orange-400 mb-2">2025年9月8日</div>
<h4 class="text-lg font-semibold mb-2">精准制裁落地</h4>
<p class="text-gray-400 text-sm">外交部依据《反外国制裁法》对日本参议员石平实施制裁,冻结资产、禁止交易、人员禁入</p>
</div>
</div>
</div>
</div>
<!-- Event 3 -->
<div class="flex items-center justify-center">
<div class="w-full md:w-5/12 text-right pr-8 hidden md:block">
<div class="gradient-border">
<div class="gradient-border-inner">
<div class="text-sm text-red-400 mb-2">2025年11月15日</div>
<h4 class="text-lg font-semibold mb-2">事态升级信号</h4>
<p class="text-gray-400 text-sm">"玉渊谭天"发布:针对高市早苗挑衅,中方表态从"敦促"升级为"一切后果由日方承担"</p>
</div>
</div>
</div>
<div class="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 bg-red-500 rounded-full border-4 border-gray-950 pulse-dot"></div>
<div class="w-full md:w-5/12 pl-8 md:hidden">
<div class="gradient-border">
<div class="gradient-border-inner">
<div class="text-sm text-red-400 mb-2">2025年11月15日</div>
<h4 class="text-lg font-semibold mb-2">事态升级信号</h4>
<p class="text-gray-400 text-sm">"玉渊谭天"发布:针对高市早苗挑衅,中方表态从"敦促"升级为"一切后果由日方承担"</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Core Logic Section -->
<div class="py-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-gray-950 to-gray-900">
<div class="max-w-7xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold mb-12 text-center">
<span class="bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">
核心逻辑三层递进
</span>
</h2>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="relative">
<div class="absolute inset-0 bg-gradient-to-r from-blue-500/20 to-transparent rounded-2xl blur-xl"></div>
<div class="relative glass-morphism rounded-2xl p-8 h-full">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
<span class="text-2xl font-bold text-blue-400">1</span>
</div>
<h3 class="ml-3 text-xl font-semibold">政治惩戒必要性</h3>
</div>
<p class="text-gray-400 mb-4">
石平案开创对日政治人物精准制裁先例,法律依据明确,手段涵盖资产冻结、人员禁入、供应链隔离,形成强力震慑。
</p>
<div class="flex flex-wrap gap-2">
<span class="px-3 py-1 rounded-full text-xs font-medium tag-primary">《反外国制裁法》</span>
<span class="px-3 py-1 rounded-full text-xs font-medium tag-primary">精准制裁</span>
<span class="px-3 py-1 rounded-full text-xs font-medium tag-primary">民意基础</span>
</div>
</div>
</div>
<div class="relative">
<div class="absolute inset-0 bg-gradient-to-r from-purple-500/20 to-transparent rounded-2xl blur-xl"></div>
<div class="relative glass-morphism rounded-2xl p-8 h-full">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center">
<span class="text-2xl font-bold text-purple-400">2</span>
</div>
<h3 class="ml-3 text-xl font-semibold">资源反制可行性</h3>
</div>
<p class="text-gray-400 mb-4">
日本在半导体材料、高性能陶瓷、OLED材料等领域深度依赖中国供应链。中方已在战略矿产实施全链条管控具备"卡脖子"能力。
</p>
<div class="flex flex-wrap gap-2">
<span class="px-3 py-1 rounded-full text-xs font-medium tag-secondary">半导体材料</span>
<span class="px-3 py-1 rounded-full text-xs font-medium tag-secondary">稀土管制</span>
<span class="px-3 py-1 rounded-full text-xs font-medium tag-secondary">全链条管控</span>
</div>
</div>
</div>
<div class="relative">
<div class="absolute inset-0 bg-gradient-to-r from-orange-500/20 to-transparent rounded-2xl blur-xl"></div>
<div class="relative glass-morphism rounded-2xl p-8 h-full">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-orange-500/20 flex items-center justify-center">
<span class="text-2xl font-bold text-orange-400">3</span>
</div>
<h3 class="ml-3 text-xl font-semibold">国力博弈战略性</h3>
</div>
<p class="text-gray-400 mb-4">
日本面临"美关税压力+国内加息+经济疲软"三重困境GDP连续负增长对华出口依存度超20%,此时反制可精准打击其执政基础。
</p>
<div class="flex flex-wrap gap-2">
<span class="px-3 py-1 rounded-full text-xs font-medium tag-accent">两线承压</span>
<span class="px-3 py-1 rounded-full text-xs font-medium tag-accent">战略筹码</span>
<span class="px-3 py-1 rounded-full text-xs font-medium tag-accent">以点破面</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Stock Data Tables -->
<div class="py-20 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold mb-12 text-center">
<span class="bg-gradient-to-r from-green-400 to-emerald-400 bg-clip-text text-transparent">
核心标的深度剖析
</span>
</h2>
<div class="space-y-8">
<!-- Semiconductor Materials -->
<div class="gradient-border">
<div class="gradient-border-inner">
<h3 class="text-2xl font-bold mb-6 flex items-center">
<i class="fas fa-microchip mr-3 text-blue-400"></i>
半导体材料板块
</h3>
<div class="stock-table">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 font-semibold text-gray-300">股票代码</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">细分领域</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">日本产业地位</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">核心竞争力</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">风险等级</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-blue-400">彤程新材</td>
<td class="py-3 px-4">ArF/KrF光刻胶</td>
<td class="py-3 px-4">日系及杜邦主导</td>
<td class="py-3 px-4">KrF量产ArF验证中</td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-yellow-500/20 text-yellow-400">中等</span></td>
</tr>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-blue-400">南大光电</td>
<td class="py-3 px-4">ArF光刻胶</td>
<td class="py-3 px-4">日系及杜邦主导</td>
<td class="py-3 px-4">通过02专项验收</td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-yellow-500/20 text-yellow-400">中等</span></td>
</tr>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-purple-400">华懋科技</td>
<td class="py-3 px-4">PSPI</td>
<td class="py-3 px-4">日本东丽、旭化成主导</td>
<td class="py-3 px-4">收购徐州博康PSPI量产</td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-red-500/20 text-red-400"></span></td>
</tr>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-purple-400">鼎龙股份</td>
<td class="py-3 px-4">PSPI</td>
<td class="py-3 px-4">日本东丽、旭化成主导</td>
<td class="py-3 px-4">PSPI产品国内领先</td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-red-500/20 text-red-400"></span></td>
</tr>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-green-400">华海诚科</td>
<td class="py-3 px-4">环氧塑封料EMC</td>
<td class="py-3 px-4">日本住友电工、力森诺科主导</td>
<td class="py-3 px-4">H1营收占比92.8%</td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-green-500/20 text-green-400"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- High Performance Ceramics -->
<div class="gradient-border">
<div class="gradient-border-inner">
<h3 class="text-2xl font-bold mb-6 flex items-center">
<i class="fas fa-cube mr-3 text-purple-400"></i>
高性能陶瓷板块
</h3>
<div class="stock-table">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 font-semibold text-gray-300">股票代码</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">细分领域</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">日本产业地位</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">技术优势</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">风险等级</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-purple-400">国瓷材料</td>
<td class="py-3 px-4">MLCC陶瓷粉体</td>
<td class="py-3 px-4">日本京瓷、TDK主导</td>
<td class="py-3 px-4">全球MLCC粉体龙头</td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-green-500/20 text-green-400"></span></td>
</tr>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-purple-400">三环集团</td>
<td class="py-3 px-4">陶瓷电容</td>
<td class="py-3 px-4">日本京瓷、TDK主导</td>
<td class="py-3 px-4">国内MLCC龙头</td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-green-500/20 text-green-400"></span></td>
</tr>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-purple-400">中瓷电子</td>
<td class="py-3 px-4">电子陶瓷外壳</td>
<td class="py-3 px-4">日本京瓷、TDK主导</td>
<td class="py-3 px-4">军用电子陶瓷领先</td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-yellow-500/20 text-yellow-400">中等</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Other Related Stocks -->
<div class="gradient-border">
<div class="gradient-border-inner">
<h3 class="text-2xl font-bold mb-6 flex items-center">
<i class="fas fa-water mr-3 text-cyan-400"></i>
其他相关板块
</h3>
<div class="stock-table">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 font-semibold text-gray-300">股票代码</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">细分领域</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">关联逻辑</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">实际受益度</th>
<th class="text-left py-3 px-4 font-semibold text-gray-300">投资建议</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-cyan-400">中水渔业</td>
<td class="py-3 px-4">水产养殖</td>
<td class="py-3 px-4">对日出口占比10%</td>
<td class="py-3 px-4"><span class="text-red-400">受损方</span></td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-red-500/20 text-red-400">规避</span></td>
</tr>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-cyan-400">乐凯胶片</td>
<td class="py-3 px-4">偏光片TAC膜</td>
<td class="py-3 px-4">唯一国产TFT-TAC膜</td>
<td class="py-3 px-4">技术突破但规模小</td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-yellow-500/20 text-yellow-400">观察</span></td>
</tr>
<tr class="border-b border-gray-800 hover:bg-gray-800/50 transition-colors">
<td class="py-3 px-4 font-medium text-cyan-400">中国稀土</td>
<td class="py-3 px-4">稀土资源</td>
<td class="py-3 px-4">潜在反制工具</td>
<td class="py-3 px-4"><span class="text-green-400"></span></td>
<td class="py-3 px-4"><span class="px-2 py-1 rounded text-xs bg-green-500/20 text-green-400">关注</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Risk Analysis -->
<div class="py-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-gray-900 to-gray-950">
<div class="max-w-7xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold mb-12 text-center">
<span class="bg-gradient-to-r from-red-400 to-orange-400 bg-clip-text text-transparent">
风险提示与挑战
</span>
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="glass-morphism rounded-2xl p-6 risk-high hover-card">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-red-500/20 flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-red-400"></i>
</div>
<h3 class="ml-3 text-lg font-semibold">技术风险</h3>
</div>
<p class="text-gray-400 text-sm mb-3">
光刻胶需12-18个月验证期国产良率低、价格高20-30%,若日方断供将导致停产风险
</p>
<div class="text-xs text-gray-500">
关键指标:良率稳定性 &lt; 90%
</div>
</div>
<div class="glass-morphism rounded-2xl p-6 risk-high hover-card">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-red-500/20 flex items-center justify-center">
<i class="fas fa-sync-alt text-red-400"></i>
</div>
<h3 class="ml-3 text-lg font-semibold">反制螺旋风险</h3>
</div>
<p class="text-gray-400 text-sm mb-3">
日本可能对半导体设备、精密仪器实施出口管制,影响长江存储、中芯国际扩产计划
</p>
<div class="text-xs text-gray-500">
关键指标:东京电子设备出口许可
</div>
</div>
<div class="glass-morphism rounded-2xl p-6 risk-medium hover-card">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-yellow-500/20 flex items-center justify-center">
<i class="fas fa-dollar-sign text-yellow-400"></i>
</div>
<h3 class="ml-3 text-lg font-semibold">商业化风险</h3>
</div>
<p class="text-gray-400 text-sm mb-3">
规模不经济导致国产替代成本高,晶圆厂替换动力不足,商业化进程缓慢
</p>
<div class="text-xs text-gray-500">
关键指标:国产替代成本差异 &gt; 15%
</div>
</div>
<div class="glass-morphism rounded-2xl p-6 risk-medium hover-card">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-yellow-500/20 flex items-center justify-center">
<i class="fas fa-users text-yellow-400"></i>
</div>
<h3 class="ml-3 text-lg font-semibold">客户集中度风险</h3>
</div>
<p class="text-gray-400 text-sm mb-3">
华海诚科等公司前五大客户占比超70%,若终端需求下滑将大幅影响业绩
</p>
<div class="text-xs text-gray-500">
关键指标CR5 &gt; 70%
</div>
</div>
<div class="glass-morphism rounded-2xl p-6 risk-low hover-card">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-green-500/20 flex items-center justify-center">
<i class="fas fa-shield-alt text-green-400"></i>
</div>
<h3 class="ml-3 text-lg font-semibold">政策持续性</h3>
</div>
<p class="text-gray-400 text-sm mb-3">
国产替代政策具有长期性,即使地缘缓和,产业链安全仍是核心诉求
</p>
<div class="text-xs text-gray-500">
关键指标:"十四五"规划延续性
</div>
</div>
<div class="glass-morphism rounded-2xl p-6 risk-low hover-card">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-green-500/20 flex items-center justify-center">
<i class="fas fa-chart-line text-green-400"></i>
</div>
<h3 class="ml-3 text-lg font-semibold">市场需求确定</h3>
</div>
<p class="text-gray-400 text-sm mb-3">
半导体、新能源等下游需求旺盛,为国产替代提供确定的市场空间
</p>
<div class="text-xs text-gray-500">
关键指标半导体CAPEX增速 &gt; 10%
</div>
</div>
</div>
</div>
</div>
<!-- Investment Strategy -->
<div class="py-20 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto">
<div class="gradient-border mb-12">
<div class="gradient-border-inner text-center">
<h2 class="text-3xl md:text-4xl font-bold mb-4">
<span class="bg-gradient-to-r from-emerald-400 to-cyan-400 bg-clip-text text-transparent">
投资策略与操作建议
</span>
</h2>
<p class="text-gray-400 text-lg">
当前阶段,观望优于参与。等待政策验证,不赌情绪溢价
</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
<div class="glass-morphism rounded-2xl p-6 hover-card">
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-r from-green-500 to-emerald-500 mb-4">
<i class="fas fa-check-circle text-white text-2xl"></i>
</div>
<h3 class="text-xl font-bold mb-2">重点配置</h3>
<p class="text-sm text-gray-400">资源管制 > 材料替代 > 水产出口</p>
</div>
<div class="space-y-3">
<div class="flex items-center">
<span class="w-2 h-2 bg-green-400 rounded-full mr-3"></span>
<span class="text-sm">中国稀土(资源定价权)</span>
</div>
<div class="flex items-center">
<span class="w-2 h-2 bg-green-400 rounded-full mr-3"></span>
<span class="text-sm">华海诚科EMC龙头</span>
</div>
<div class="flex items-center">
<span class="w-2 h-2 bg-green-400 rounded-full mr-3"></span>
<span class="text-sm">鼎龙股份PSPI量产</span>
</div>
</div>
</div>
<div class="glass-morphism rounded-2xl p-6 hover-card">
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-r from-yellow-500 to-orange-500 mb-4">
<i class="fas fa-eye text-white text-2xl"></i>
</div>
<h3 class="text-xl font-bold mb-2">观察等待</h3>
<p class="text-sm text-gray-400">技术突破但规模尚小</p>
</div>
<div class="space-y-3">
<div class="flex items-center">
<span class="w-2 h-2 bg-yellow-400 rounded-full mr-3"></span>
<span class="text-sm">彤程新材(光刻胶验证)</span>
</div>
<div class="flex items-center">
<span class="w-2 h-2 bg-yellow-400 rounded-full mr-3"></span>
<span class="text-sm">乐凯胶片TAC膜研发</span>
</div>
<div class="flex items-center">
<span class="w-2 h-2 bg-yellow-400 rounded-full mr-3"></span>
<span class="text-sm">南大光电ArF进展</span>
</div>
</div>
</div>
<div class="glass-morphism rounded-2xl p-6 hover-card">
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-r from-red-500 to-pink-500 mb-4">
<i class="fas fa-times-circle text-white text-2xl"></i>
</div>
<h3 class="text-xl font-bold mb-2">坚决规避</h3>
<p class="text-sm text-gray-400">逻辑错误,将直接受损</p>
</div>
<div class="space-y-3">
<div class="flex items-center">
<span class="w-2 h-2 bg-red-400 rounded-full mr-3"></span>
<span class="text-sm">中水渔业(水产出口)</span>
</div>
<div class="flex items-center">
<span class="w-2 h-2 bg-red-400 rounded-full mr-3"></span>
<span class="text-sm">国联水产(对日依赖)</span>
</div>
<div class="flex items-center">
<span class="w-2 h-2 bg-red-400 rounded-full mr-3"></span>
<span class="text-sm">獐子岛(贸易风险)</span>
</div>
</div>
</div>
</div>
<!-- Key Metrics -->
<div class="glass-morphism rounded-2xl p-8">
<h3 class="text-2xl font-bold mb-6">关键跟踪指标</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="text-center">
<div class="text-3xl font-bold number-stat mb-2">≥2次/月</div>
<div class="text-sm text-gray-400">部级官员提及反制</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold number-stat mb-2">≥1项</div>
<div class="text-sm text-gray-400">贸易救济立案</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold number-stat mb-2">>30天</div>
<div class="text-sm text-gray-400">出口许可周期</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold number-stat mb-2">>15%</div>
<div class="text-sm text-gray-400">概念股换手率</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="py-12 px-4 border-t border-gray-800">
<div class="max-w-7xl mx-auto text-center">
<p class="text-gray-500 text-sm">
* 本分析基于公开信息整理,不构成投资建议。投资有风险,决策需谨慎。
</p>
<p class="text-gray-600 text-xs mt-4">
数据来源:外交部、产业库、公司财报、机构调研 | 更新时间2025年11月
</p>
</div>
</footer>
<script>
// Add smooth scroll behavior
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
// Add intersection observer for fade-in animations
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
document.querySelectorAll('.hover-card').forEach(card => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
card.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(card);
});
</script>
</body>
</html>

604
public/htmls/忆阻器.html Normal file
View File

@@ -0,0 +1,604 @@
我将为您生成一个专业、详实的忆阻器概念分析页面,融合金融分析深度与现代前端美学。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>忆阻器 - 下一代AI硬件的颠覆性力量</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
* {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.tech-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.tech-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
.timeline-line {
background: linear-gradient(180deg, transparent, #667eea, #764ba2, transparent);
}
.pulse-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.5);
}
}
.stock-table {
font-size: 14px;
}
.stock-table th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
}
.risk-badge {
animation: glow 2s ease-in-out infinite alternate;
}
@keyframes glow {
from { box-shadow: 0 0 5px #ef4444; }
to { box-shadow: 0 0 20px #ef4444, 0 0 30px #ef4444; }
}
.hero-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
overflow: hidden;
}
.hero-gradient::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: rotate 30s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.tech-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
@media (max-width: 768px) {
.stock-table {
font-size: 12px;
}
.tech-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body class="bg-gray-50">
<!-- Hero Section -->
<header class="hero-gradient text-white relative">
<div class="container mx-auto px-4 py-16 relative z-10">
<div class="text-center">
<div class="inline-block mb-4">
<span class="glass-effect px-4 py-2 rounded-full text-sm">
<i class="fas fa-microchip mr-2"></i>前沿科技 · 金融洞察
</span>
</div>
<h1 class="text-5xl md:text-6xl font-bold mb-6">
忆阻器
<span class="block text-3xl md:text-4xl mt-2 opacity-90">
Memristor - 破解AI算力能耗之谜
</span>
</h1>
<p class="text-xl max-w-3xl mx-auto mb-8 opacity-90">
第四代无源电路元件存算一体架构核心将AI芯片能耗降低57.2%
</p>
<div class="flex flex-wrap justify-center gap-4">
<span class="bg-white/20 px-4 py-2 rounded-lg">
<i class="fas fa-chart-line mr-2"></i>Gartner技术成熟度期望膨胀期
</span>
<span class="bg-white/20 px-4 py-2 rounded-lg">
<i class="fas fa-fire mr-2"></i>市场热度:脉冲式上升
</span>
<span class="bg-white/20 px-4 py-2 rounded-lg">
<i class="fas fa-shield-alt mr-2"></i>投资策略:早布局 · 长周期
</span>
</div>
</div>
</div>
</header>
<!-- 核心观点摘要 -->
<section class="py-16 bg-white">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12 gradient-text">
核心观点摘要
</h2>
<div class="tech-grid">
<div class="tech-card bg-gradient-to-br from-purple-50 to-indigo-50 p-6 rounded-xl border border-purple-200">
<div class="text-purple-600 mb-4">
<i class="fas fa-bolt text-3xl"></i>
</div>
<h3 class="text-xl font-semibold mb-3">技术突破验证期</h3>
<p class="text-gray-700">
已从理论验证进入工程化突破关键期AI芯片能耗锐减57.2%,脑机接口实现四自由度操控
</p>
</div>
<div class="tech-card bg-gradient-to-br from-blue-50 to-cyan-50 p-6 rounded-xl border border-blue-200">
<div class="text-blue-600 mb-4">
<i class="fas fa-industry text-3xl"></i>
</div>
<h3 class="text-xl font-semibold mb-3">0到1的幼稚期</h3>
<p class="text-gray-700">
产业整体处于"0到1"幼稚期,代工厂缺失构成最大瓶颈,创业公司被迫自建产线
</p>
</div>
<div class="tech-card bg-gradient-to-br from-green-50 to-emerald-50 p-6 rounded-xl border border-green-200">
<div class="text-green-600 mb-4">
<i class="fas fa-chart-line text-3xl"></i>
</div>
<h3 class="text-xl font-semibold mb-3">研报零覆盖</h3>
<p class="text-gray-700">
市场关注度极低6份研报零提及存在巨大预期差属于典型的"高壁垒、长周期"主题
</p>
</div>
<div class="tech-card bg-gradient-to-br from-orange-50 to-red-50 p-6 rounded-xl border border-orange-200">
<div class="text-orange-600 mb-4">
<i class="fas fa-exclamation-triangle text-3xl"></i>
</div>
<h3 class="text-xl font-semibold mb-3">核心风险</h3>
<p class="text-gray-700">
代工厂断链、材料可靠性、技术路线竞争,从实验室到商业化仍需跨越"死亡谷"
</p>
</div>
</div>
</div>
</section>
<!-- 技术突破时间轴 -->
<section class="py-16 bg-gray-100">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
<span class="gradient-text">技术突破时间轴</span>
</h2>
<div class="relative max-w-4xl mx-auto">
<div class="timeline-line absolute left-8 md:left-1/2 transform md:-translate-x-1/2 w-1 h-full"></div>
<!-- 2023年5月 -->
<div class="relative flex items-center mb-12">
<div class="absolute left-8 md:left-1/2 transform md:-translate-x-1/2 w-4 h-4 bg-indigo-600 rounded-full pulse-dot"></div>
<div class="ml-16 md:ml-0 md:w-1/2 md:pr-8 md:text-right">
<div class="bg-white p-4 rounded-lg shadow-lg">
<h3 class="font-semibold text-indigo-600">2023年5月29日</h3>
<p class="text-gray-700">中国移动联合清华大学完成业界首次忆阻器存算一体芯片端到端技术验证</p>
</div>
</div>
</div>
<!-- 2025年2月 -->
<div class="relative flex items-center mb-12 md:flex-row-reverse">
<div class="absolute left-8 md:left-1/2 transform md:-translate-x-1/2 w-4 h-4 bg-purple-600 rounded-full pulse-dot"></div>
<div class="ml-16 md:ml-0 md:w-1/2 md:pl-8">
<div class="bg-white p-4 rounded-lg shadow-lg">
<h3 class="font-semibold text-purple-600">2025年2月17日</h3>
<p class="text-gray-700">清华-天大团队在《自然·电子》发表基于忆阻器的"双环路"脑机接口系统</p>
</div>
</div>
</div>
<!-- 2025年7月 -->
<div class="relative flex items-center mb-12">
<div class="absolute left-8 md:left-1/2 transform md:-translate-x-1/2 w-4 h-4 bg-green-600 rounded-full pulse-dot"></div>
<div class="ml-16 md:ml-0 md:w-1/2 md:pr-8 md:text-right">
<div class="bg-white p-4 rounded-lg shadow-lg">
<h3 class="font-semibold text-green-600">2025年7月29日</h3>
<p class="text-gray-700">福晶科技披露少量供应镍酸锂晶体,忆阻器关键功能层材料量产突破</p>
</div>
</div>
</div>
<!-- 2025年11月 -->
<div class="relative flex items-center mb-12">
<div class="absolute left-8 md:left-1/2 transform md:-translate-x-1/2 w-4 h-4 bg-red-600 rounded-full pulse-dot"></div>
<div class="ml-16 md:ml-0 md:w-1/2 md:pr-8 md:text-right">
<div class="bg-white p-4 rounded-lg shadow-lg">
<h3 class="font-semibold text-red-600">2025年11月17日</h3>
<p class="text-gray-700">港大团队宣布忆阻器AI芯片能耗锐减57.2%,能效比展现颠覆性潜力</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 产业链图谱 -->
<section class="py-16 bg-white">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12 gradient-text">
产业链图谱
</h2>
<div class="bg-gradient-to-r from-indigo-50 to-purple-50 rounded-xl p-8 mb-8">
<div class="grid md:grid-cols-3 gap-6">
<div class="text-center">
<div class="bg-white rounded-lg p-6 shadow-md">
<i class="fas fa-layer-group text-4xl text-indigo-600 mb-4"></i>
<h3 class="font-semibold text-lg mb-2">上游:材料与设备</h3>
<p class="text-sm text-gray-600">功能材料、制造设备、EDA工具</p>
<div class="mt-4 space-y-2">
<div class="text-sm bg-indigo-100 rounded px-2 py-1">天通股份(镍酸锂薄膜)</div>
<div class="text-sm bg-indigo-100 rounded px-2 py-1">福晶科技(晶体供应)</div>
</div>
</div>
</div>
<div class="text-center">
<div class="bg-white rounded-lg p-6 shadow-md">
<i class="fas fa-microchip text-4xl text-purple-600 mb-4"></i>
<h3 class="font-semibold text-lg mb-2">中游:器件与芯片</h3>
<p class="text-sm text-gray-600">技术验证、设计制造、代工厂</p>
<div class="mt-4 space-y-2">
<div class="text-sm bg-purple-100 rounded px-2 py-1">亿铸科技(POC芯片)</div>
<div class="text-sm bg-red-100 rounded px-2 py-1">代工厂:空缺(核心瓶颈)</div>
</div>
</div>
</div>
<div class="text-center">
<div class="bg-white rounded-lg p-6 shadow-md">
<i class="fas fa-robot text-4xl text-green-600 mb-4"></i>
<h3 class="font-semibold text-lg mb-2">下游:系统与应用</h3>
<p class="text-sm text-gray-600">脑机接口、AI芯片、边缘设备</p>
<div class="mt-4 space-y-2">
<div class="text-sm bg-green-100 rounded px-2 py-1">中国移动(边缘计算)</div>
<div class="text-sm bg-green-100 rounded px-2 py-1">清华-天大(脑机接口)</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 股票数据表格 -->
<section class="py-16 bg-gray-100">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12 gradient-text">
核心相关公司
</h2>
<div class="bg-white rounded-xl shadow-xl overflow-hidden">
<div class="overflow-x-auto">
<table class="stock-table w-full">
<thead>
<tr>
<th class="px-4 py-3 text-left">股票名称</th>
<th class="px-4 py-3 text-left">产业链定位</th>
<th class="px-4 py-3 text-left">具体项目</th>
<th class="px-4 py-3 text-left">信源</th>
<th class="px-4 py-3 text-left">纯度评级</th>
<th class="px-4 py-3 text-left">关键逻辑</th>
</tr>
</thead>
<tbody>
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-semibold text-indigo-600">天通股份</td>
<td class="px-4 py-3">镍酸锂晶片薄膜(材料)</td>
<td class="px-4 py-3">大尺寸镍酸锂晶片</td>
<td class="px-4 py-3">互动/研报</td>
<td class="px-4 py-3">
<span class="bg-yellow-500 text-white px-2 py-1 rounded">★★★★★</span>
</td>
<td class="px-4 py-3 text-sm">销量国内第一,受益于忆阻器放量的核心材料商</td>
</tr>
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-semibold text-indigo-600">盛视科技</td>
<td class="px-4 py-3">参股亿铸科技(3.42%)</td>
<td class="px-4 py-3">基于忆阻器ReRAM的POC芯片</td>
<td class="px-4 py-3">公告</td>
<td class="px-4 py-3">
<span class="bg-blue-500 text-white px-2 py-1 rounded">★★★☆☆</span>
</td>
<td class="px-4 py-3 text-sm">财务投资+业务协同,参股国内领先忆阻器芯片公司</td>
</tr>
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-semibold text-indigo-600">中国移动</td>
<td class="px-4 py-3">产品验证方</td>
<td class="px-4 py-3">忆阻器存算一体芯片端到端技术验证</td>
<td class="px-4 py-3">新闻</td>
<td class="px-4 py-3">
<span class="bg-blue-500 text-white px-2 py-1 rounded">★★★☆☆</span>
</td>
<td class="px-4 py-3 text-sm">央企龙头,完成技术验证,具备采购拉动能力</td>
</tr>
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-semibold text-indigo-600">福晶科技</td>
<td class="px-4 py-3">镍酸锂晶体(材料)</td>
<td class="px-4 py-3">少量供应镍酸锂晶体</td>
<td class="px-4 py-3">互动</td>
<td class="px-4 py-3">
<span class="bg-orange-500 text-white px-2 py-1 rounded">★★☆☆☆</span>
</td>
<td class="px-4 py-3 text-sm">少量供应,非核心业务,贡献有限</td>
</tr>
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-semibold text-indigo-600">金百泽</td>
<td class="px-4 py-3">设计制造服务</td>
<td class="px-4 py-3">高校忆阻器研究的电子服务</td>
<td class="px-4 py-3">互动</td>
<td class="px-4 py-3">
<span class="bg-orange-500 text-white px-2 py-1 rounded">★★☆☆☆</span>
</td>
<td class="px-4 py-3 text-sm">"卖铲人"角色,为学术团队提供原型服务</td>
</tr>
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-semibold text-indigo-600">风华高科</td>
<td class="px-4 py-3">产品</td>
<td class="px-4 py-3">忆阻器研究</td>
<td class="px-4 py-3">互动</td>
<td class="px-4 py-3">
<span class="bg-gray-500 text-white px-2 py-1 rounded">★☆☆☆☆</span>
</td>
<td class="px-4 py-3 text-sm">明确无专项投入,概念纯度最低</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- 风险与挑战 -->
<section class="py-16 bg-white">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12 gradient-text">
核心风险与挑战
</h2>
<div class="grid md:grid-cols-2 gap-8">
<div class="bg-red-50 rounded-xl p-6 border-l-4 border-red-500">
<h3 class="text-xl font-semibold text-red-700 mb-4">
<i class="fas fa-exclamation-circle mr-2"></i>技术风险
</h3>
<ul class="space-y-3 text-gray-700">
<li class="flex items-start">
<i class="fas fa-times-circle text-red-500 mt-1 mr-2"></i>
<span>代工厂缺失:创业公司被迫自建产线,良率可能&lt;30%</span>
</li>
<li class="flex items-start">
<i class="fas fa-times-circle text-red-500 mt-1 mr-2"></i>
<span>材料可靠性循环寿命仅10⁵次远低于DRAM的10¹⁵次</span>
</li>
<li class="flex items-start">
<i class="fas fa-times-circle text-red-500 mt-1 mr-2"></i>
<span>集成密度当前1Kb-1Mb商业需&gt;1Gb差3个数量级</span>
</li>
</ul>
</div>
<div class="bg-orange-50 rounded-xl p-6 border-l-4 border-orange-500">
<h3 class="text-xl font-semibold text-orange-700 mb-4">
<i class="fas fa-coins mr-2"></i>商业化风险
</h3>
<ul class="space-y-3 text-gray-700">
<li class="flex items-start">
<i class="fas fa-times-circle text-orange-500 mt-1 mr-2"></i>
<span>成本倒挂:自建产线成本&gt;$100边缘AI芯片仅$5-10</span>
</li>
<li class="flex items-start">
<i class="fas fa-times-circle text-orange-500 mt-1 mr-2"></i>
<span>市场错配:脑机接口市场规模&lt;1亿无法支撑量产</span>
</li>
<li class="flex items-start">
<i class="fas fa-times-circle text-orange-500 mt-1 mr-2"></i>
<span>路线竞争SRAM/MRAM/PCRAM多路线竞争忆阻器无绝对优势</span>
</li>
</ul>
</div>
<div class="bg-yellow-50 rounded-xl p-6 border-l-4 border-yellow-500">
<h3 class="text-xl font-semibold text-yellow-700 mb-4">
<i class="fas fa-gavel mr-2"></i>政策与竞争风险
</h3>
<ul class="space-y-3 text-gray-700">
<li class="flex items-start">
<i class="fas fa-times-circle text-yellow-500 mt-1 mr-2"></i>
<span>政策优先度:未在"卡脖子"清单明确提及</span>
</li>
<li class="flex items-start">
<i class="fas fa-times-circle text-yellow-500 mt-1 mr-2"></i>
<span>国际竞争IBM/惠普专利储备领先,可能封锁代工渠道</span>
</li>
<li class="flex items-start">
<i class="fas fa-times-circle text-yellow-500 mt-1 mr-2"></i>
<span>知识产权:基础专利到期,但工艺改进专利仍在快速申请</span>
</li>
</ul>
</div>
<div class="bg-purple-50 rounded-xl p-6 border-l-4 border-purple-500">
<h3 class="text-xl font-semibold text-purple-700 mb-4">
<i class="fas fa-chart-line mr-2"></i>投资风险
</h3>
<ul class="space-y-3 text-gray-700">
<li class="flex items-start">
<i class="fas fa-times-circle text-purple-500 mt-1 mr-2"></i>
<span>预期差:新闻乐观叙事与路演负面现实形成矛盾</span>
</li>
<li class="flex items-start">
<i class="fas fa-times-circle text-purple-500 mt-1 mr-2"></i>
<span>信息真空0篇研报覆盖财务模型无法建立</span>
</li>
<li class="flex items-start">
<i class="fas fa-times-circle text-purple-500 mt-1 mr-2"></i>
<span>概念混淆:市场存在大量"伪概念股"需仔细甄别</span>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 投资建议 -->
<section class="py-16 bg-gradient-to-br from-indigo-600 to-purple-600 text-white">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
投资策略建议
</h2>
<div class="grid md:grid-cols-3 gap-8">
<div class="glass-effect rounded-xl p-6">
<h3 class="text-2xl font-bold mb-4">
<i class="fas fa-lightbulb mr-2"></i>当前策略
</h3>
<p class="mb-4">
轻仓配置天通股份,作为"不对称期权"。仓位控制2-3%,向下有底,向上空间巨大。
</p>
<div class="bg-white/10 rounded-lg p-4">
<p class="text-sm">
天通股份作为材料龙头,无论技术路线如何,薄膜外延材料都是刚性需求。
</p>
</div>
</div>
<div class="glass-effect rounded-xl p-6">
<h3 class="text-2xl font-bold mb-4">
<i class="fas fa-rocket mr-2"></i>加仓条件
</h3>
<ul class="space-y-2 mb-4">
<li>✓ 中芯国际宣布忆阻器工艺平台</li>
<li>✓ 天通股份单季采购额>1000万</li>
<li>✓ 科技部设立忆阻器重大专项</li>
</ul>
<div class="bg-green-500/20 rounded-lg p-4">
<p class="text-sm">
满足任一条件仓位可提升至5-8%
</p>
</div>
</div>
<div class="glass-effect rounded-xl p-6">
<h3 class="text-2xl font-bold mb-4">
<i class="fas fa-shield-alt mr-2"></i>风控纪律
</h3>
<p class="mb-4">
若2年内未现代工厂合作或亿铸科技等头部公司倒闭应清仓离场。
</p>
<div class="bg-red-500/20 rounded-lg p-4">
<p class="text-sm">
保持70%观察、20%研究、10%参与的"狙击手"姿态
</p>
</div>
</div>
</div>
<div class="mt-12 text-center">
<p class="text-xl opacity-90">
忆阻器的命运不取决于技术多先进而取决于产业生态能否在2026年前疏通
</p>
<div class="mt-4 flex justify-center gap-4">
<span class="bg-white/20 px-4 py-2 rounded-lg">
<i class="fas fa-clock mr-2"></i>耐心等待基本面质变
</span>
<span class="bg-white/20 px-4 py-2 rounded-lg">
<i class="fas fa-chart-pie mr-2"></i>不宜重仓 · 适合配置
</span>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-gray-900 text-gray-300 py-8">
<div class="container mx-auto px-4 text-center">
<p class="mb-2">
<i class="fas fa-info-circle mr-2"></i>
本页面基于公开信息整理,不构成投资建议
</p>
<p class="text-sm">
数据来源:新闻、路演、公司公告 | 更新时间2025年11月
</p>
</div>
</footer>
<script>
// 添加滚动动画
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
document.querySelectorAll('.tech-card, section').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(el);
});
// 平滑滚动
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,736 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>林地资源:战略资产的三重价值重构</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.glass-morphism {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.timeline-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(99, 102, 241, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
}
}
.gradient-text {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.number-highlight {
font-size: 2.5rem;
font-weight: 800;
line-height: 1;
}
.progress-bar {
position: relative;
overflow: hidden;
}
.progress-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.sticky-nav {
position: sticky;
top: 0;
z-index: 40;
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.95);
}
.section-divider {
height: 1px;
background: linear-gradient(90deg, transparent, #e5e7eb, transparent);
margin: 3rem 0;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<div class="sticky-nav shadow-lg">
<div class="navbar bg-base-100 px-4 lg:px-8">
<div class="flex-1">
<a href="#" class="btn btn-ghost text-xl font-bold gradient-text">
<i class="fas fa-tree mr-2"></i>林地资源分析
</a>
</div>
<div class="flex-none">
<button class="btn btn-circle btn-ghost">
<i class="fas fa-chart-line"></i>
</button>
<button class="btn btn-circle btn-ghost">
<i class="fas fa-share-alt"></i>
</button>
</div>
</div>
</div>
<!-- Hero Section -->
<section class="relative overflow-hidden">
<div class="container mx-auto px-4 py-16">
<div class="glass-morphism rounded-3xl p-8 lg:p-12">
<div class="flex flex-col lg:flex-row items-center gap-8">
<div class="flex-1 text-white">
<div class="badge badge-warning badge-lg mb-4">战略资产重构</div>
<h1 class="text-4xl lg:text-6xl font-bold mb-6">
林地资源:从传统资产到<span class="text-yellow-300">三重价值</span>跃迁
</h1>
<p class="text-lg lg:text-xl mb-8 opacity-90">
政策市场化改革 + 产业链上游紧缩 + 碳汇价值商业化,开启林地资源新时代
</p>
<div class="flex flex-wrap gap-4">
<div class="stat bg-white/20 rounded-lg px-4 py-3">
<div class="stat-title text-white/80">2024年</div>
<div class="stat-value text-2xl text-white">政策破冰期</div>
</div>
<div class="stat bg-white/20 rounded-lg px-4 py-3">
<div class="stat-title text-white/80">2025年</div>
<div class="stat-value text-2xl text-white">碳汇元年</div>
</div>
<div class="stat bg-white/20 rounded-lg px-4 py-3">
<div class="stat-title text-white/80">2027年</div>
<div class="stat-value text-2xl text-white">价值爆发期</div>
</div>
</div>
</div>
<div class="flex-1">
<div class="bg-white/10 rounded-2xl p-6">
<canvas id="trendChart" width="400" height="300"></canvas>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 时间轴 -->
<section class="container mx-auto px-4 py-12">
<h2 class="text-3xl font-bold text-white mb-8">概念事件与背景时间轴</h2>
<div class="timeline">
<div class="relative">
<!-- 时间轴线 -->
<div class="absolute left-8 top-0 bottom-0 w-0.5 bg-white/30"></div>
<!-- 时间节点 -->
<div class="relative flex items-center mb-8">
<div class="timeline-dot absolute left-8 w-4 h-4 bg-yellow-400 rounded-full -translate-x-1/2"></div>
<div class="ml-16 glass-morphism rounded-xl p-6 card-hover flex-1">
<div class="text-yellow-400 font-bold text-lg mb-2">2024年6月</div>
<h3 class="text-xl font-semibold text-white mb-2">政策破冰期</h3>
<p class="text-white/80">国家林草局印发《集体林地经营权流转管理办法》,确立市场化机制</p>
</div>
</div>
<div class="relative flex items-center mb-8">
<div class="timeline-dot absolute left-8 w-4 h-4 bg-blue-400 rounded-full -translate-x-1/2"></div>
<div class="ml-16 glass-morphism rounded-xl p-6 card-hover flex-1">
<div class="text-blue-400 font-bold text-lg mb-2">2023-2024年</div>
<h3 class="text-xl font-semibold text-white mb-2">产业觉醒期</h3>
<p class="text-white/80">全球新增商品浆产能超800万吨木片资源日趋紧张</p>
</div>
</div>
<div class="relative flex items-center mb-8">
<div class="timeline-dot absolute left-8 w-4 h-4 bg-green-400 rounded-full -translate-x-1/2"></div>
<div class="ml-16 glass-morphism rounded-xl p-6 card-hover flex-1">
<div class="text-green-400 font-bold text-lg mb-2">2024年9月10日</div>
<h3 class="text-xl font-semibold text-white mb-2">碳汇商业化元年</h3>
<p class="text-white/80">CCER交易规则最终确定岳阳林纸中标云南254.4万亩林业碳汇项目</p>
</div>
</div>
<div class="relative flex items-center">
<div class="timeline-dot absolute left-8 w-4 h-4 bg-red-400 rounded-full -translate-x-1/2"></div>
<div class="ml-16 glass-morphism rounded-xl p-6 card-hover flex-1">
<div class="text-red-400 font-bold text-lg mb-2">2024年</div>
<h3 class="text-xl font-semibold text-white mb-2">地缘政治映射</h3>
<p class="text-white/80">印尼通过林地许可查封部分镍矿,林地成为资源国调控工具</p>
</div>
</div>
</div>
</div>
</section>
<!-- 核心观点 -->
<section class="container mx-auto px-4 py-12">
<h2 class="text-3xl font-bold text-white mb-8">核心观点摘要</h2>
<div class="grid md:grid-cols-2 gap-6">
<div class="glass-morphism rounded-2xl p-6 card-hover">
<div class="text-6xl mb-4">📊</div>
<h3 class="text-2xl font-bold text-white mb-3">市场认知严重滞后</h3>
<p class="text-white/80 mb-4">10篇主流研报中0篇涉及林地资源关注度处于冰点</p>
<div class="flex gap-2">
<span class="badge badge-error">预期差极大</span>
<span class="badge badge-warning">主题发酵期</span>
</div>
</div>
<div class="glass-morphism rounded-2xl p-6 card-hover">
<div class="text-6xl mb-4">💰</div>
<h3 class="text-2xl font-bold text-white mb-3">三重价值重构</h3>
<p class="text-white/80 mb-4">造纸产业链成本护城河 + 双碳战略绿色金融资产 + 地缘政策缓冲器</p>
<div class="flex gap-2">
<span class="badge badge-success">成本优势</span>
<span class="badge badge-info">碳汇价值</span>
</div>
</div>
</div>
<div class="mt-8 p-8 bg-gradient-to-r from-purple-500/20 to-pink-500/20 rounded-2xl border border-white/20">
<p class="text-xl text-white font-medium italic">
"林地资源已不再是传统意义上的农林资产,而是造纸产业链的'成本护城河'、双碳战略下的'绿色金融资产'以及资源民族主义的'政策缓冲器'"
</p>
</div>
</section>
<!-- 核心逻辑 -->
<section class="container mx-auto px-4 py-12">
<h2 class="text-3xl font-bold text-white mb-8">三重核心驱动力</h2>
<div class="space-y-8">
<!-- 政策市场化 -->
<div class="glass-morphism rounded-2xl p-8">
<div class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 bg-yellow-500 rounded-full flex items-center justify-center">
<i class="fas fa-gavel text-white text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-white">政策市场化改革</h3>
</div>
<div class="grid md:grid-cols-3 gap-4 mt-6">
<div class="bg-white/10 rounded-lg p-4">
<div class="text-yellow-400 font-semibold mb-2">确权到流转闭环</div>
<p class="text-white/80 text-sm">林地可从分散农户集中至企业,实现规模化经营</p>
</div>
<div class="bg-white/10 rounded-lg p-4">
<div class="text-yellow-400 font-semibold mb-2">金融属性激活</div>
<p class="text-white/80 text-sm">经营权可抵押、可融资,资产估值转向现金流折现</p>
</div>
<div class="bg-white/10 rounded-lg p-4">
<div class="text-yellow-400 font-semibold mb-2">流动性溢价</div>
<p class="text-white/80 text-sm">拥有流转渠道的企业将获得流动性溢价</p>
</div>
</div>
</div>
<!-- 产业链紧缩 -->
<div class="glass-morphism rounded-2xl p-8">
<div class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center">
<i class="fas fa-industry text-white text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-white">产业链上游资源紧缩</h3>
</div>
<div class="grid md:grid-cols-2 gap-6 mt-6">
<div>
<div class="text-3xl font-bold text-blue-400 mb-2">2027年前无新增项目</div>
<p class="text-white/80">全球新增商品浆产能超800万吨下一个大型项目需等到2027年</p>
</div>
<div>
<div class="text-3xl font-bold text-blue-400 mb-2">成本差100美元+</div>
<p class="text-white/80">太阳纸业自制木片成本50-100美元/吨外购价150-200美元/吨</p>
</div>
</div>
<div class="mt-6">
<div class="flex justify-between text-white/80 mb-2">太阳纸业老挝木片自供率</div>
<div class="w-full bg-white/20 rounded-full h-6 progress-bar">
<div class="bg-gradient-to-r from-blue-400 to-blue-600 h-6 rounded-full flex items-center justify-center text-white font-semibold" style="width: 60%">
60% (2024年)
</div>
</div>
</div>
</div>
<!-- 碳汇价值 -->
<div class="glass-morphism rounded-2xl p-8">
<div class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 bg-green-500 rounded-full flex items-center justify-center">
<i class="fas fa-leaf text-white text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-white">碳汇价值商业化</h3>
</div>
<div class="stats shadow w-full">
<div class="stat place-items-center bg-white/10 rounded-lg">
<div class="stat-title">碳汇毛利率</div>
<div class="stat-value text-green-400">70-80%</div>
</div>
<div class="stat place-items-center bg-white/10 rounded-lg">
<div class="stat-title">岳阳林纸储备</div>
<div class="stat-value text-green-400">8000万亩+</div>
</div>
<div class="stat place-items-center bg-white/10 rounded-lg">
<div class="stat-title">CCER价格预期</div>
<div class="stat-value text-green-400">年增30-40%</div>
</div>
</div>
</div>
</div>
</section>
<!-- 核心公司对比 -->
<section class="container mx-auto px-4 py-12">
<h2 class="text-3xl font-bold text-white mb-8">核心玩家深度对比</h2>
<div class="overflow-x-auto">
<table class="table w-full glass-morphism rounded-2xl overflow-hidden">
<thead class="bg-white/20">
<tr>
<th class="text-white">公司</th>
<th class="text-white">核心逻辑</th>
<th class="text-white">资源规模</th>
<th class="text-white">商业模式</th>
<th class="text-white">竞争优势</th>
<th class="text-white">投资价值</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-white/10">
<td class="font-bold text-yellow-400">太阳纸业</td>
<td>林浆纸一体化</td>
<td>老挝6万公顷</td>
<td>木片自给降本</td>
<td>16年深耕成本优势</td>
<td><span class="badge badge-success badge-lg">★★★★★</span></td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold text-green-400">岳阳林纸</td>
<td>林业碳汇第一股</td>
<td>8000万亩储备</td>
<td>碳汇高毛利</td>
<td>央企背景,开发能力</td>
<td><span class="badge badge-success badge-lg">★★★★☆</span></td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold text-blue-400">仙鹤股份</td>
<td>特种纸原料锁定</td>
<td>100万亩桉树</td>
<td>政府排他支持</td>
<td>运输成本最低</td>
<td><span class="badge badge-warning badge-lg">★★★★☆</span></td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">永安林业</td>
<td>纯资源持有</td>
<td>114.1万亩</td>
<td>传统采伐</td>
<td>区域垄断</td>
<td><span class="badge badge-warning badge-lg">★★☆☆☆</span></td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">东珠生态</td>
<td>碳汇开发服务</td>
<td>421.5万亩</td>
<td>轻资产模式</td>
<td>快速扩张</td>
<td><span class="badge badge-error badge-lg">★★☆☆☆</span></td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- 股票数据表格 -->
<section class="container mx-auto px-4 py-12">
<h2 class="text-3xl font-bold text-white mb-8">林地资源相关股票数据</h2>
<div class="glass-morphism rounded-2xl p-6 overflow-x-auto">
<table class="table table-zebra w-full text-white">
<thead class="bg-white/20">
<tr>
<th class="text-white">股票代码</th>
<th class="text-white">股票名称</th>
<th class="text-white">分类</th>
<th class="text-white">项目详情</th>
<th class="text-white">地理位置</th>
<th class="text-white">股东背景</th>
<th class="text-white">持股比例</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-white/10">
<td class="font-bold">岳阳林纸</td>
<td>岳阳林纸</td>
<td><span class="badge badge-info">自有林地</span></td>
<td>公司拥有林业面积180万亩</td>
<td>-</td>
<td>国务院国资委</td>
<td>30.9%</td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">岳阳林纸</td>
<td>岳阳林纸</td>
<td><span class="badge badge-success">林业碳汇</span></td>
<td>累计完成项目储备8246.79万亩</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">康欣新材</td>
<td>康欣新材</td>
<td><span class="badge badge-info">自有林地</span></td>
<td>速生杨林地等138万余亩</td>
<td>-</td>
<td>无锡国资委</td>
<td>19.86%</td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">永安林业</td>
<td>永安林业</td>
<td><span class="badge badge-info">自有林地</span></td>
<td>森林资源总面积114.1万亩</td>
<td>福建永安市</td>
<td>国务院国资委</td>
<td>5.31%</td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">福建金森</td>
<td>福建金森</td>
<td><span class="badge badge-info">自有林地</span></td>
<td>森林资源面积近80万亩</td>
<td>福建省将乐县</td>
<td>将乐县财政局</td>
<td>63.83%</td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">平潭发展</td>
<td>平潭发展</td>
<td><span class="badge badge-info">自有林地</span></td>
<td>自有林地约60万亩、托管约30万亩</td>
<td>福建省平潭综合实验区</td>
<td>-</td>
<td>-</td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">丰林集团</td>
<td>丰林集团</td>
<td><span class="badge badge-info">自有林地</span></td>
<td>拥有近20万亩产权林地</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">东珠生态</td>
<td>东珠生态</td>
<td><span class="badge badge-success">林业碳汇</span></td>
<td>开发面积林地421.5万亩</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">海南橡胶</td>
<td>海南橡胶</td>
<td><span class="badge badge-warning">其他</span></td>
<td>公司胶园土地390万亩</td>
<td>-</td>
<td>海南国资委</td>
<td>57.92%</td>
</tr>
<tr class="hover:bg-white/10">
<td class="font-bold">泉阳泉</td>
<td>泉阳泉</td>
<td><span class="badge badge-warning">其他</span></td>
<td>控股股东拥有林地128.96万公顷</td>
<td>-</td>
<td>吉林国资委</td>
<td>15.99%</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- 关键催化剂 -->
<section class="container mx-auto px-4 py-12">
<h2 class="text-3xl font-bold text-white mb-8">关键催化剂</h2>
<div class="grid md:grid-cols-2 gap-6">
<div class="bg-gradient-to-br from-yellow-500/20 to-orange-500/20 glass-morphism rounded-2xl p-6 border border-yellow-500/30">
<div class="flex items-center gap-3 mb-4">
<i class="fas fa-clock text-yellow-400 text-2xl"></i>
<h3 class="text-xl font-bold text-white">近期催化剂3-6个月</h3>
</div>
<ul class="space-y-3 text-white/80">
<li class="flex items-start gap-2">
<i class="fas fa-circle text-xs mt-2 text-yellow-400"></i>
<span>岳阳林纸2024年报碳汇利润达5000万+</span>
</li>
<li class="flex items-start gap-2">
<i class="fas fa-circle text-xs mt-2 text-yellow-400"></i>
<span>CCER价格落地80-100元/吨</span>
</li>
<li class="flex items-start gap-2">
<i class="fas fa-circle text-xs mt-2 text-yellow-400"></i>
<span>太阳纸业中报:木片自供率提升</span>
</li>
<li class="flex items-start gap-2">
<i class="fas fa-circle text-xs mt-2 text-yellow-400"></i>
<span>仙鹤股份:广西林地协议签署</span>
</li>
</ul>
</div>
<div class="bg-gradient-to-br from-blue-500/20 to-purple-500/20 glass-morphism rounded-2xl p-6 border border-blue-500/30">
<div class="flex items-center gap-3 mb-4">
<i class="fas fa-rocket text-blue-400 text-2xl"></i>
<h3 class="text-xl font-bold text-white">长期发展路径2025-2030</h3>
</div>
<div class="space-y-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white font-bold">1</div>
<div>
<div class="text-white font-semibold">2025-2026商业模式兑现期</div>
<div class="text-white/60 text-sm">木片自给率80%+碳汇收入占比15-20%</div>
</div>
</div>
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center text-white font-bold">2</div>
<div>
<div class="text-white font-semibold">2027-2028证券化与金融化</div>
<div class="text-white/60 text-sm">林地经营权ABS、碳汇期货</div>
</div>
</div>
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-pink-500 rounded-full flex items-center justify-center text-white font-bold">3</div>
<div>
<div class="text-white font-semibold">2029-2030产业整合</div>
<div class="text-white/60 text-sm">国内并购潮,海外基地复制</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 风险提示 -->
<section class="container mx-auto px-4 py-12">
<h2 class="text-3xl font-bold text-white mb-8">潜在风险与挑战</h2>
<div class="grid md:grid-cols-3 gap-6">
<div class="glass-morphism rounded-2xl p-6 border-red-500/30 border">
<div class="flex items-center gap-3 mb-4">
<i class="fas fa-exclamation-triangle text-red-400 text-xl"></i>
<h3 class="text-xl font-bold text-white">政策风险</h3>
</div>
<ul class="text-white/80 space-y-2 text-sm">
<li>• CCER政策变动</li>
<li>• 海外林地租约不确定性</li>
<li>• 碳汇价格波动</li>
</ul>
</div>
<div class="glass-morphism rounded-2xl p-6 border-orange-500/30 border">
<div class="flex items-center gap-3 mb-4">
<i class="fas fa-tools text-orange-400 text-xl"></i>
<h3 class="text-xl font-bold text-white">执行风险</h3>
</div>
<ul class="text-white/80 space-y-2 text-sm">
<li>• 砍伐面积爬坡不及预期</li>
<li>• 碳汇项目签发周期长</li>
<li>• 本土化运营挑战</li>
</ul>
</div>
<div class="glass-morphism rounded-2xl p-6 border-yellow-500/30 border">
<div class="flex items-center gap-3 mb-4">
<i class="fas fa-globe text-yellow-400 text-xl"></i>
<h3 class="text-xl font-bold text-white">地缘风险</h3>
</div>
<ul class="text-white/80 space-y-2 text-sm">
<li>• 资源民族主义蔓延</li>
<li>• 政权更迭影响</li>
<li>• 社区关系维护</li>
</ul>
</div>
</div>
</section>
<!-- 投资建议 -->
<section class="container mx-auto px-4 py-12 mb-12">
<h2 class="text-3xl font-bold text-white mb-8">投资启示</h2>
<div class="glass-morphism rounded-2xl p-8 bg-gradient-to-br from-green-500/20 to-blue-500/20 border-green-400/30 border">
<div class="grid md:grid-cols-2 gap-8">
<div>
<h3 class="text-2xl font-bold text-white mb-4">阶段判断</h3>
<p class="text-white/80 mb-4">
当前处于"主题发酵期向基本面兑现期过渡"的关键节点。与纯主题概念不同,林地资源具备明确的成本节约和利润贡献。
</p>
<div class="flex gap-2">
<span class="badge badge-success">PB普遍1-2倍</span>
<span class="badge badge-info">修复空间>50%</span>
</div>
</div>
<div>
<h3 class="text-2xl font-bold text-white mb-4">核心配置</h3>
<div class="space-y-3">
<div class="flex items-center justify-between bg-white/10 rounded-lg p-3">
<span class="text-white font-semibold">第一顺位</span>
<span class="text-yellow-400">太阳纸业</span>
</div>
<div class="flex items-center justify-between bg-white/10 rounded-lg p-3">
<span class="text-white font-semibold">第二顺位</span>
<span class="text-green-400">岳阳林纸</span>
</div>
<div class="flex items-center justify-between bg-white/10 rounded-lg p-3">
<span class="text-white font-semibold">第三顺位</span>
<span class="text-blue-400">仙鹤股份</span>
</div>
</div>
</div>
</div>
<div class="mt-8 p-6 bg-white/10 rounded-xl">
<p class="text-xl text-white font-medium text-center">
"2025年是林地资源基本面兑现元年静待估值修复与戴维斯双击"
</p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="glass-morphism py-8">
<div class="container mx-auto px-4 text-center text-white/80">
<p>© 2024 林地资源概念分析 | 数据来源:公开信息整理</p>
<p class="mt-2 text-sm">投资有风险,入市需谨慎</p>
</div>
</footer>
<script>
// 趋势图表
const ctx = document.getElementById('trendChart').getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.5)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0.1)');
new Chart(ctx, {
type: 'line',
data: {
labels: ['2023', '2024', '2025', '2026', '2027', '2028'],
datasets: [{
label: '林地资源价值指数',
data: [100, 135, 180, 250, 350, 480],
borderColor: 'rgba(255, 255, 255, 0.8)',
backgroundColor: gradient,
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'rgba(255, 255, 255, 0.2)',
borderWidth: 1
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: 'rgba(255, 255, 255, 0.8)'
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: 'rgba(255, 255, 255, 0.8)'
}
}
}
}
});
// 平滑滚动
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
// 滚动动画
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
document.querySelectorAll('.card-hover').forEach(card => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
card.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(card);
});
</script>
</body>
</html>

675
public/htmls/牛肉.html Normal file
View File

@@ -0,0 +1,675 @@
我将为您创建一个专业且炫酷的牛肉概念投资分析页面。这个页面将整合所有深度insight、新闻数据和股票信息采用现代化的设计风格。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>牛肉产业投资分析 - 周期反转与全球共振</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
* {
font-family: 'Inter', sans-serif;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.dark-gradient {
background: linear-gradient(180deg, #1a1c20 0%, #2d3436 100%);
}
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
}
.timeline-line {
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
width: 2px;
position: absolute;
left: 50%;
transform: translateX(-50%);
height: 100%;
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.scroll-smooth {
scroll-behavior: smooth;
}
.text-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.neon-border {
box-shadow: 0 0 20px rgba(102, 126, 234, 0.5),
inset 0 0 20px rgba(102, 126, 234, 0.1);
}
.data-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
@media (max-width: 768px) {
.timeline-line {
left: 20px;
}
}
.table-container {
overflow-x: auto;
}
.sticky-header {
position: sticky;
top: 0;
z-index: 10;
}
</style>
</head>
<body class="bg-gray-900 text-white scroll-smooth">
<!-- Hero Section -->
<section class="gradient-bg py-20 px-6">
<div class="max-w-7xl mx-auto">
<div class="text-center">
<h1 class="text-5xl md:text-6xl font-bold mb-6 text-white">
<i class="fas fa-chart-line mr-4"></i>牛肉产业投资分析
</h1>
<p class="text-xl md:text-2xl text-purple-100 mb-8">周期反转与全球共振的黄金投资机遇</p>
<div class="flex flex-wrap justify-center gap-4 mb-8">
<span class="bg-white/20 px-6 py-3 rounded-full text-lg">
<i class="fas fa-arrow-trend-up mr-2"></i>价格弹性: +70-80%
</span>
<span class="bg-white/20 px-6 py-3 rounded-full text-lg">
<i class="fas fa-calendar mr-2"></i>周期长度: 2-3年
</span>
<span class="bg-white/20 px-6 py-3 rounded-full text-lg">
<i class="fas fa-globe mr-2"></i>全球共振
</span>
</div>
</div>
</div>
</section>
<!-- 核心观点摘要 -->
<section class="py-16 px-6 dark-gradient">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold mb-8 text-gradient">核心观点</h2>
<div class="data-card rounded-2xl p-8 neon-border">
<p class="text-xl leading-relaxed">
当前牛肉概念处于<span class="text-purple-400 font-bold">周期反转初期</span>,驱动逻辑从政策主题转向基本面改善。
<span class="text-purple-400 font-bold">未来2-3年景气度将持续上行</span>,价格弹性或超预期。
</p>
<div class="grid md:grid-cols-3 gap-6 mt-8">
<div class="bg-gradient-to-r from-purple-600/20 to-pink-600/20 p-6 rounded-xl">
<i class="fas fa-industry text-3xl text-purple-400 mb-3"></i>
<h3 class="text-xl font-semibold mb-2">深度去化</h3>
<p class="text-gray-300">母牛存栏降3% · 犊牛降8% · 产业根基受损</p>
</div>
<div class="bg-gradient-to-r from-blue-600/20 to-cyan-600/20 p-6 rounded-xl">
<i class="fas fa-shield-halved text-3xl text-blue-400 mb-3"></i>
<h3 class="text-xl font-semibold mb-2">政策约束</h3>
<p class="text-gray-300">商务部保障措施 · 关税加征 · 进口收缩</p>
</div>
<div class="bg-gradient-to-r from-green-600/20 to-emerald-600/20 p-6 rounded-xl">
<i class="fas fa-earth-americas text-3xl text-green-400 mb-3"></i>
<h3 class="text-xl font-semibold mb-2">全球共振</h3>
<p class="text-gray-300">国际价格涨40% · 美国供给缺口 · 巴西减产</p>
</div>
</div>
</div>
</div>
</section>
<!-- 时间轴 -->
<section class="py-16 px-6 bg-gray-800">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold mb-12 text-center">核心事件时间轴</h2>
<div class="relative">
<div class="timeline-line"></div>
<!-- 2020-2022 -->
<div class="flex items-center mb-12">
<div class="w-1/2 md:pr-8 text-right">
<div class="data-card p-6 rounded-xl card-hover">
<h3 class="text-2xl font-bold mb-2 text-purple-400">2020-2022年</h3>
<p class="text-lg">景气高点与产能扩张</p>
<ul class="mt-3 text-gray-300 text-sm">
<li>• 牛肉价格达历史峰值 86.21元/公斤</li>
<li>• 2020-2024年产量CAGR 3.5%</li>
<li>• 地方政府鼓励大规模补栏</li>
</ul>
</div>
</div>
<div class="w-12 h-12 bg-purple-600 rounded-full flex items-center justify-center pulse">
<i class="fas fa-arrow-up text-white"></i>
</div>
<div class="w-1/2 md:pl-8"></div>
</div>
<!-- 2023年 -->
<div class="flex items-center mb-12">
<div class="w-1/2 md:pr-8"></div>
<div class="w-12 h-12 bg-red-600 rounded-full flex items-center justify-center pulse">
<i class="fas fa-arrow-down text-white"></i>
</div>
<div class="w-1/2 md:pl-8">
<div class="data-card p-6 rounded-xl card-hover">
<h3 class="text-2xl font-bold mb-2 text-red-400">2023年</h3>
<p class="text-lg">供需反转,价格崩塌</p>
<ul class="mt-3 text-gray-300 text-sm">
<li>• 3月开启持续下跌跌幅超13%</li>
<li>• 能繁母牛存栏下降1.9%</li>
<li>• 产能去化信号初现</li>
</ul>
</div>
</div>
</div>
<!-- 2024年 -->
<div class="flex items-center mb-12">
<div class="w-1/2 md:pr-8 text-right">
<div class="data-card p-6 rounded-xl card-hover">
<h3 class="text-2xl font-bold mb-2 text-orange-400">2024年</h3>
<p class="text-lg">深度亏损,产业动摇</p>
<ul class="mt-3 text-gray-300 text-sm">
<li>• 连续8个月亏损超1000元/头</li>
<li>• 65%以上养殖场户亏损</li>
<li>• 12月商务部启动保障措施调查</li>
</ul>
</div>
</div>
<div class="w-12 h-12 bg-orange-600 rounded-full flex items-center justify-center pulse">
<i class="fas fa-exclamation-triangle text-white"></i>
</div>
<div class="w-1/2 md:pl-8"></div>
</div>
<!-- 2025年 -->
<div class="flex items-center">
<div class="w-1/2 md:pr-8"></div>
<div class="w-12 h-12 bg-green-600 rounded-full flex items-center justify-center pulse">
<i class="fas fa-chart-line text-white"></i>
</div>
<div class="w-1/2 md:pl-8">
<div class="data-card p-6 rounded-xl card-hover">
<h3 class="text-2xl font-bold mb-2 text-green-400">2025年</h3>
<p class="text-lg">周期拐点显现</p>
<ul class="mt-3 text-gray-300 text-sm">
<li>• 1-2月进口量同比下降17.4%</li>
<li>• 3月起价格淡季不淡上涨8.67%</li>
<li>• 11月预期政策落地核心催化剂</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 供需与价格分析 -->
<section class="py-16 px-6 dark-gradient">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold mb-12">供需格局深度分析</h2>
<div class="grid md:grid-cols-2 gap-8 mb-12">
<!-- 供给端 -->
<div class="data-card p-8 rounded-2xl">
<h3 class="text-2xl font-bold mb-6 text-purple-400">
<i class="fas fa-boxes-stacked mr-3"></i>供给端变化
</h3>
<div class="space-y-4">
<div>
<div class="flex justify-between mb-2">
<span>能繁母牛存栏</span>
<span class="text-red-400">↓ 3%</span>
</div>
<div class="bg-gray-700 rounded-full h-3">
<div class="bg-gradient-to-r from-red-500 to-red-600 h-3 rounded-full" style="width: 3%"></div>
</div>
</div>
<div>
<div class="flex justify-between mb-2">
<span>新生犊牛</span>
<span class="text-red-400">↓ 8%</span>
</div>
<div class="bg-gray-700 rounded-full h-3">
<div class="bg-gradient-to-r from-red-500 to-red-600 h-3 rounded-full" style="width: 8%"></div>
</div>
</div>
<div>
<div class="flex justify-between mb-2">
<span>全国牛存栏</span>
<span class="text-red-400">↓ 4.4%</span>
</div>
<div class="bg-gray-700 rounded-full h-3">
<div class="bg-gradient-to-r from-red-500 to-red-600 h-3 rounded-full" style="width: 4.4%"></div>
</div>
</div>
<div>
<div class="flex justify-between mb-2">
<span>进口量</span>
<span class="text-red-400">↓ 17.4%</span>
</div>
<div class="bg-gray-700 rounded-full h-3">
<div class="bg-gradient-to-r from-red-500 to-red-600 h-3 rounded-full" style="width: 17.4%"></div>
</div>
</div>
</div>
</div>
<!-- 价格预测 -->
<div class="data-card p-8 rounded-2xl">
<h3 class="text-2xl font-bold mb-6 text-green-400">
<i class="fas fa-chart-column mr-3"></i>价格走势预测
</h3>
<canvas id="priceChart" width="400" height="200"></canvas>
<div class="mt-6 space-y-2">
<p class="text-sm"><span class="text-green-400">2025年:</span> 26-28元/公斤 (↑10-15%)</p>
<p class="text-sm"><span class="text-yellow-400">2026年:</span> 32-35元/公斤 (↑30-40%)</p>
<p class="text-sm"><span class="text-red-400">2027年:</span> 38-42元/公斤 (↑70-80%)</p>
</div>
</div>
</div>
</div>
</section>
<!-- 核心催化剂 -->
<section class="py-16 px-6 bg-gray-800">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold mb-12 text-center">关键催化剂</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="data-card p-6 rounded-xl text-center card-hover">
<div class="text-5xl mb-4 text-purple-400">
<i class="fas fa-gavel"></i>
</div>
<h3 class="text-xl font-bold mb-3">政策落地</h3>
<p class="text-gray-300">2025年11月商务部保障措施终裁</p>
<div class="mt-4 text-2xl font-bold text-purple-400">核心催化</div>
</div>
<div class="data-card p-6 rounded-xl text-center card-hover">
<div class="text-5xl mb-4 text-blue-400">
<i class="fas fa-snowflake"></i>
</div>
<h3 class="text-xl font-bold mb-3">消费旺季</h3>
<p class="text-gray-300">Q4传统旺季需求环比提升30%</p>
<div class="mt-4 text-2xl font-bold text-blue-400">季节催化</div>
</div>
<div class="data-card p-6 rounded-xl text-center card-hover">
<div class="text-5xl mb-4 text-green-400">
<i class="fas fa-globe"></i>
</div>
<h3 class="text-xl font-bold mb-3">海外涨价</h3>
<p class="text-gray-300">国际价格已涨40%,传导效应显现</p>
<div class="mt-4 text-2xl font-bold text-green-400">外部催化</div>
</div>
<div class="data-card p-6 rounded-xl text-center card-hover">
<div class="text-5xl mb-4 text-orange-400">
<i class="fas fa-file-invoice-dollar"></i>
</div>
<h3 class="text-xl font-bold mb-3">业绩预增</h3>
<p class="text-gray-300">龙头Q3利润或增200%+</p>
<div class="mt-4 text-2xl font-bold text-orange-400">业绩催化</div>
</div>
</div>
</div>
</section>
<!-- 产业链分析 -->
<section class="py-16 px-6 dark-gradient">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold mb-12">产业链价值分布</h2>
<div class="grid md:grid-cols-3 gap-8">
<!-- 上游 -->
<div class="data-card p-8 rounded-2xl text-center">
<h3 class="text-2xl font-bold mb-4 text-purple-400">上游</h3>
<p class="text-lg mb-4">种源与饲料</p>
<div class="text-3xl font-bold mb-4">15-25% 毛利率</div>
<div class="space-y-2 text-left">
<p>• 天山生物(育种)</p>
<p>• 禾丰股份、海大集团(饲料)</p>
<p>• 福成股份、西部牧业(养殖)</p>
</div>
</div>
<!-- 中游 -->
<div class="data-card p-8 rounded-2xl text-center">
<h3 class="text-2xl font-bold mb-4 text-blue-400">中游</h3>
<p class="text-lg mb-4">屠宰加工</p>
<div class="text-3xl font-bold mb-4">6-13% 毛利率</div>
<div class="space-y-2 text-left">
<p>• 光明肉业(银蕨农场)</p>
<p>• 得利斯(产能利用率关键)</p>
</div>
</div>
<!-- 下游 -->
<div class="data-card p-8 rounded-2xl text-center">
<h3 class="text-2xl font-bold mb-4 text-green-400">下游</h3>
<p class="text-lg mb-4">品牌与渠道</p>
<div class="text-3xl font-bold mb-4">25-35% 毛利率</div>
<div class="space-y-2 text-left">
<p>• 福成股份(肥牛品牌)</p>
<p>• 光明肉业(银蕨品牌)</p>
<p>• 味知香(预制菜)</p>
</div>
</div>
</div>
</div>
</section>
<!-- 核心公司股票数据 -->
<section class="py-16 px-6 bg-gray-800">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold mb-12 text-center">核心上市公司分析</h2>
<div class="table-container">
<table class="w-full data-card rounded-lg overflow-hidden">
<thead class="sticky-header bg-gradient-to-r from-purple-600 to-pink-600">
<tr>
<th class="p-4 text-left">股票代码</th>
<th class="p-4 text-left">股票名称</th>
<th class="p-4 text-left">业务分类</th>
<th class="p-4 text-left">核心业务</th>
<th class="p-4 text-left">投资逻辑</th>
<th class="p-4 text-center">推荐评级</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-700 hover:bg-gray-700/50 transition">
<td class="p-4">600965</td>
<td class="p-4 font-bold text-green-400">福成股份</td>
<td class="p-4"><span class="bg-purple-600/30 px-2 py-1 rounded">养殖</span></td>
<td class="p-4 text-sm">三个养牛场活牛存栏容纳能力达到四万头2025年上半年活牛存栏3.44万头</td>
<td class="p-4 text-sm">全产业链区域龙头,华北渠道强,"福成肥牛"品牌知名度高</td>
<td class="p-4 text-center">
<span class="text-2xl">⭐⭐⭐⭐</span>
</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700/50 transition">
<td class="p-4">300313</td>
<td class="p-4 font-bold text-blue-400">天山生物</td>
<td class="p-4"><span class="bg-orange-600/30 px-2 py-1 rounded">育种</span></td>
<td class="p-4 text-sm">我国唯一专业从事牛品种改良的上市公司,将打造自有鲜牛肉品牌</td>
<td class="p-4 text-sm">稀缺育种标的,掌握核心种源,长期成长确定</td>
<td class="p-4 text-center">
<span class="text-2xl">⭐⭐⭐</span>
</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700/50 transition">
<td class="p-4">600073</td>
<td class="p-4 font-bold text-purple-400">光明肉业</td>
<td class="p-4"><span class="bg-green-600/30 px-2 py-1 rounded">加工</span></td>
<td class="p-4 text-sm">控股新西兰银蕨农场市占率30%2025Q1净利润同比+312%</td>
<td class="p-4 text-sm">海外资源龙头,直接受益全球涨价,业绩弹性最大</td>
<td class="p-4 text-center">
<span class="text-2xl">⭐⭐⭐⭐⭐</span>
</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700/50 transition">
<td class="p-4">002330</td>
<td class="p-4 font-bold text-yellow-400">得利斯</td>
<td class="p-4"><span class="bg-blue-600/30 px-2 py-1 rounded">加工</span></td>
<td class="p-4 text-sm">主要业务之一为牛肉精深加工行业</td>
<td class="p-4 text-sm">纯屠宰企业,当前仍亏损,产能利用率是关键</td>
<td class="p-4 text-center">
<span class="text-2xl">⭐⭐</span>
</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700/50 transition">
<td class="p-4">605089</td>
<td class="p-4 font-bold text-cyan-400">味知香</td>
<td class="p-4"><span class="bg-green-600/30 px-2 py-1 rounded">预制菜</span></td>
<td class="p-4 text-sm">牛肉类产品占比较大(预制菜)</td>
<td class="p-4 text-sm">受益消费升级,牛肉预制菜需求增长</td>
<td class="p-4 text-center">
<span class="text-2xl">⭐⭐⭐</span>
</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700/50 transition">
<td class="p-4">300106</td>
<td class="p-4 font-bold text-green-400">西部牧业</td>
<td class="p-4"><span class="bg-purple-600/30 px-2 py-1 rounded">养殖</span></td>
<td class="p-4 text-sm">建设肉牛专业育肥基地,与肉牛养殖专业合作社建立合作关系</td>
<td class="p-4 text-sm">乳肉兼营,受益原奶+牛肉双周期</td>
<td class="p-4 text-center">
<span class="text-2xl">⭐⭐⭐</span>
</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700/50 transition">
<td class="p-4">000061</td>
<td class="p-4 font-bold text-red-400">农产品</td>
<td class="p-4"><span class="bg-red-600/30 px-2 py-1 rounded">贸易</span></td>
<td class="p-4 text-sm">引进的南美牛肉等核心品类的规模稳健增长</td>
<td class="p-4 text-sm">进口贸易商,政策收紧直接受损,建议规避</td>
<td class="p-4 text-center">
<span class="text-2xl"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 投资策略 -->
<section class="py-16 px-6 dark-gradient">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold mb-12 text-center">投资策略建议</h2>
<div class="grid md:grid-cols-3 gap-8 mb-12">
<!-- 第一梯队 -->
<div class="data-card p-8 rounded-2xl neon-border">
<div class="text-3xl font-bold mb-4 text-green-400">第一梯队</div>
<div class="text-xl mb-4">海外资源型(首选)</div>
<div class="bg-green-600/20 p-4 rounded-lg mb-4">
<p class="font-bold text-green-400 mb-2">核心标的:光明肉业</p>
<p class="text-sm">逻辑最顺 · 弹性最大</p>
<p class="text-sm mt-2">2027年利润或达5亿元对应PE仅5-6倍</p>
</div>
<div class="text-sm space-y-1">
<p>• 仓位建议60%</p>
<p>• 持有周期至2027年景气高点</p>
</div>
</div>
<!-- 第二梯队 -->
<div class="data-card p-8 rounded-2xl neon-border">
<div class="text-3xl font-bold mb-4 text-blue-400">第二梯队</div>
<div class="text-xl mb-4">全产业链型(次优)</div>
<div class="bg-blue-600/20 p-4 rounded-lg mb-4">
<p class="font-bold text-blue-400 mb-2">核心标的:福成股份</p>
<p class="text-sm">双轮驱动 · 品牌护城河</p>
<p class="text-sm mt-2">存栏3.44万头具备扩张潜力</p>
</div>
<div class="text-sm space-y-1">
<p>• 仓位建议30%</p>
<p>• 持有周期:中期配置</p>
</div>
</div>
<!-- 第三梯队 -->
<div class="data-card p-8 rounded-2xl neon-border">
<div class="text-3xl font-bold mb-4 text-purple-400">第三梯队</div>
<div class="text-xl mb-4">育种核心(长期)</div>
<div class="bg-purple-600/20 p-4 rounded-lg mb-4">
<p class="font-bold text-purple-400 mb-2">核心标的:天山生物</p>
<p class="text-sm">产业升级 · 种源卡脖子</p>
<p class="text-sm mt-2">技术壁垒高,业绩释放慢</p>
</div>
<div class="text-sm space-y-1">
<p>• 仓位建议10%</p>
<p>• 持有周期:卫星仓位</p>
</div>
</div>
</div>
<!-- 风险提示 -->
<div class="bg-red-900/30 border border-red-600 p-6 rounded-xl">
<h3 class="text-2xl font-bold mb-4 text-red-400">
<i class="fas fa-triangle-exclamation mr-2"></i>风险提示
</h3>
<div class="grid md:grid-cols-2 gap-6">
<div>
<h4 class="font-bold mb-2 text-red-300">需求风险</h4>
<ul class="text-sm text-gray-300 space-y-1">
<li>• 消费降级导致高价蛋白需求萎缩</li>
<li>• 餐饮端恢复不及预期</li>
</ul>
</div>
<div>
<h4 class="font-bold mb-2 text-red-300">政策风险</h4>
<ul class="text-sm text-gray-300 space-y-1">
<li>• 保障措施力度不及预期</li>
<li>• 贸易反制措施</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-gray-900 border-t border-gray-800 py-8 px-6">
<div class="max-w-7xl mx-auto text-center">
<p class="text-gray-400">© 2025 牛肉产业投资分析报告 | 数据来源:公开信息整理</p>
<p class="text-gray-500 text-sm mt-2">投资有风险,入市需谨慎</p>
</div>
</footer>
<script>
// 价格走势图表
const ctx = document.getElementById('priceChart').getContext('2d');
const priceChart = new Chart(ctx, {
type: 'line',
data: {
labels: ['2024', '2025', '2026', '2027', '2028'],
datasets: [{
label: '牛肉价格预测(元/公斤)',
data: [24, 27, 34, 40, 38],
borderColor: 'rgb(168, 85, 247)',
backgroundColor: 'rgba(168, 85, 247, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
plugins: {
legend: {
labels: {
color: 'white'
}
}
},
scales: {
y: {
beginAtZero: false,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: 'white'
}
},
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: 'white'
}
}
}
}
});
// 滚动动画
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
// 为所有卡片添加滚动动画
document.querySelectorAll('.card-hover').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(el);
});
// 平滑滚动
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
</script>
</body>
</html>
这个HTML页面提供了一个专业、炫酷且功能丰富的牛肉产业投资分析界面。主要特点包括
## 🎯 核心功能
1. **完整的内容展示** - 整合了所有insight分析、新闻数据和股票信息
2. **交互式时间轴** - 展示核心事件演进过程
3. **数据可视化** - 包含价格走势图表和供需进度条
4. **详细股票表格** - 清晰展示相关上市公司信息,支持水平滚动
5. **投资策略建议** - 分梯队展示投资标的和建议
## 💫 设计亮点
- **渐变色主题** - 紫色到粉色的渐变,营造专业金融感
- **玻璃态效果** - 半透明卡片配合模糊背景
- **霓虹光效** - 重要元素添加发光边框
- **响应式布局** - 完美适配桌面和移动设备
- **平滑动画** - 滚动触发的渐入效果
- **微交互** - 悬停效果和脉冲动画
## 📊 数据展示
- 三重共振驱动逻辑的可视化
- 供需端变化进度条
- 价格预测折线图
- 产业链价值分布
- 核心公司评级表格
页面保留了insight的绝大部分关键内容包括周期分析、产能去化、政策影响、全球格局等核心观点并通过现代化的设计语言让信息更易理解和吸收。

624
public/htmls/羽绒.html Normal file
View File

@@ -0,0 +1,624 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>羽绒行业深度分析报告</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.24/dist/full.min.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
* {
font-family: 'Inter', sans-serif;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.timeline-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(147, 51, 234, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(147, 51, 234, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(147, 51, 234, 0);
}
}
.hover-scale {
transition: transform 0.3s ease;
}
.hover-scale:hover {
transform: scale(1.05);
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.table-container {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.custom-scrollbar::-webkit-scrollbar {
height: 8px;
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #1f2937;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
@media (max-width: 768px) {
.hero-text {
font-size: 2rem !important;
}
}
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
}
.price-up {
animation: priceUp 1s ease-out;
}
@keyframes priceUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>
</head>
<body class="bg-gray-900 text-white">
<!-- Hero Section -->
<div class="gradient-bg min-h-screen flex items-center justify-center relative overflow-hidden">
<div class="absolute inset-0 bg-black opacity-50"></div>
<div class="container mx-auto px-6 relative z-10">
<div class="text-center">
<h1 class="hero-text text-6xl md:text-8xl font-bold mb-6 text-white animate-pulse">
羽绒行业
</h1>
<p class="text-2xl md:text-3xl mb-8 text-gray-200">供需失衡下的史诗级行情</p>
<div class="flex justify-center space-x-4 mb-12">
<div class="glass-effect rounded-lg px-6 py-3">
<i class="fas fa-chart-line text-yellow-400 mr-2"></i>
<span class="text-xl font-semibold">价格涨幅 <span class="text-3xl">241%</span></span>
</div>
<div class="glass-effect rounded-lg px-6 py-3">
<i class="fas fa-snowflake text-blue-400 mr-2"></i>
<span class="text-xl font-semibold">冷冬催化</span>
</div>
<div class="glass-effect rounded-lg px-6 py-3">
<i class="fas fa-industry text-green-400 mr-2"></i>
<span class="text-xl font-semibold">产业升级</span>
</div>
</div>
<button onclick="scrollToSection('timeline')" class="bg-purple-600 hover:bg-purple-700 px-8 py-4 rounded-full text-xl font-semibold transition-all hover-scale">
深度解析 <i class="fas fa-arrow-down ml-2"></i>
</button>
</div>
</div>
<div class="absolute bottom-0 left-0 right-0">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
<path fill="#111827" fill-opacity="1" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,117.3C672,107,768,117,864,138.7C960,160,1056,192,1152,192C1248,192,1344,160,1392,144L1440,128L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path>
</svg>
</div>
</div>
<!-- Timeline Section -->
<section id="timeline" class="py-20 bg-gray-900">
<div class="container mx-auto px-6">
<h2 class="text-4xl md:text-5xl font-bold text-center mb-16 gradient-text">概念事件时间轴</h2>
<div class="space-y-8">
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-12 h-12 bg-purple-600 rounded-full flex items-center justify-center timeline-dot">
<span class="text-sm font-bold">2023</span>
</div>
<div class="flex-grow glass-effect rounded-lg p-6 card-hover">
<h3 class="text-xl font-semibold mb-2">价格基础奠定期</h3>
<p class="text-gray-300">白鸭绒价格从3,500元/吨涨至5,380元/吨涨幅超53%。供给端产能去化,需求端冰雪经济推动。</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-12 h-12 bg-red-600 rounded-full flex items-center justify-center timeline-dot">
<span class="text-sm font-bold">2024.11</span>
</div>
<div class="flex-grow glass-effect rounded-lg p-6 card-hover">
<h3 class="text-xl font-semibold mb-2">央视曝光造假事件</h3>
<p class="text-gray-300">央视曝光羽绒造假黑幕,以"飞丝"冒充羽绒成本差距50%+。行业监管趋严,劣币出清加速。</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-12 h-12 bg-green-600 rounded-full flex items-center justify-center timeline-dot">
<span class="text-sm font-bold">2025.05</span>
</div>
<div class="flex-grow glass-effect rounded-lg p-6 card-hover">
<h3 class="text-xl font-semibold mb-2">古麒绒材上市</h3>
<p class="text-gray-300">羽绒材料龙头古麒绒材登陆A股上市首日大涨222.85%触发临停,概念情绪达到高点。</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-12 h-12 bg-yellow-600 rounded-full flex items-center justify-center timeline-dot">
<span class="text-sm font-bold">2025.10-11</span>
</div>
<div class="flex-grow glass-effect rounded-lg p-6 card-hover price-up">
<h3 class="text-xl font-semibold mb-2">价格暴涨期</h3>
<p class="text-gray-300">寒潮提前来临90%白鸭绒价格从17万元/吨飙升至58万元/吨鹅绒达98万元/吨。</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-12 h-12 bg-blue-600 rounded-full flex items-center justify-center timeline-dot">
<span class="text-sm font-bold">2025.11.20</span>
</div>
<div class="flex-grow glass-effect rounded-lg p-6 card-hover">
<h3 class="text-xl font-semibold mb-2">机构密集推荐</h3>
<p class="text-gray-300">多家机构同步推荐波司登,关注古麒绒材,明确"冷冬逐步确认"交易逻辑。</p>
</div>
</div>
</div>
</div>
</section>
<!-- Core Logic Section -->
<section class="py-20 bg-gray-800">
<div class="container mx-auto px-6">
<h2 class="text-4xl md:text-5xl font-bold text-center mb-16 gradient-text">核心逻辑三维解析</h2>
<div class="grid md:grid-cols-3 gap-8">
<div class="glass-effect rounded-xl p-8 card-hover">
<div class="text-4xl mb-4">📉</div>
<h3 class="text-2xl font-bold mb-4 text-purple-400">供给侧刚性收缩</h3>
<ul class="space-y-2 text-gray-300">
<li>• 养殖产能历史低位40-42亿羽</li>
<li>• 较高峰期减少16.7%</li>
<li>• 环保政策挤出中小屠宰场</li>
<li>• 羽毛分流效应(羽毛球原料)</li>
</ul>
</div>
<div class="glass-effect rounded-xl p-8 card-hover">
<div class="text-4xl mb-4">📈</div>
<h3 class="text-2xl font-bold mb-4 text-green-400">需求侧结构性升级</h3>
<ul class="space-y-2 text-gray-300">
<li>• 冰雪经济1.5万亿市场空间</li>
<li>• 新国标实际成本增加15-20%</li>
<li>• 高充绒量300克/件)成标配</li>
<li>• 渗透率仅9%vs 欧美35%</li>
</ul>
</div>
<div class="glass-effect rounded-xl p-8 card-hover">
<div class="text-4xl mb-4">⚖️</div>
<h3 class="text-2xl font-bold mb-4 text-blue-400">监管侧劣币出清</h3>
<ul class="space-y-2 text-gray-300">
<li>• 央视曝光行业造假乱象</li>
<li>• 成本铁底420-550元/千克</li>
<li>• 从"价格战"转向"价值战"</li>
<li>• 头部企业市占率加速提升</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Industry Chain Section -->
<section class="py-20 bg-gray-900">
<div class="container mx-auto px-6">
<h2 class="text-4xl md:text-5xl font-bold text-center mb-16 gradient-text">产业链图谱</h2>
<div class="overflow-x-auto custom-scrollbar">
<div class="flex space-x-6 pb-4" style="min-width: max-content;">
<!-- 上游 -->
<div class="bg-gray-800 rounded-lg p-6 min-w-[280px] card-hover">
<h3 class="text-xl font-bold mb-4 text-purple-400">上游:水禽养殖与屠宰</h3>
<div class="space-y-3">
<div class="bg-gray-700 rounded p-3">
<div class="font-semibold">益客食品</div>
<div class="text-sm text-gray-400">全球第二大肉鸭供应商</div>
<div class="text-sm text-gray-400">年屠宰3.5亿只</div>
</div>
<div class="bg-gray-700 rounded p-3">
<div class="font-semibold">立华股份</div>
<div class="text-sm text-gray-400">商品鹅年销210万只</div>
<div class="text-sm text-gray-400">鹅绒高端原料</div>
</div>
<div class="bg-gray-700 rounded p-3">
<div class="font-semibold">华英农业</div>
<div class="text-sm text-gray-400">自产约8%原毛</div>
<div class="text-sm text-gray-400">其余90%外采</div>
</div>
</div>
</div>
<!-- 中游 -->
<div class="bg-gray-800 rounded-lg p-6 min-w-[280px] card-hover">
<h3 class="text-xl font-bold mb-4 text-green-400">中游:羽绒加工与水洗</h3>
<div class="space-y-3">
<div class="bg-gray-700 rounded p-3">
<div class="font-semibold">古麒绒材</div>
<div class="text-sm text-gray-400">A股次新龙头</div>
<div class="text-sm text-gray-400">产能2288吨</div>
</div>
<div class="bg-gray-700 rounded p-3">
<div class="font-semibold">华英农业</div>
<div class="text-sm text-gray-400">市占率国内15%</div>
<div class="text-sm text-gray-400">全球接近10%</div>
</div>
<div class="bg-gray-700 rounded p-3">
<div class="font-semibold">柳桥集团</div>
<div class="text-sm text-gray-400">非上市龙头</div>
<div class="text-sm text-gray-400">出口占比高</div>
</div>
</div>
</div>
<!-- 下游 -->
<div class="bg-gray-800 rounded-lg p-6 min-w-[320px] card-hover">
<h3 class="text-xl font-bold mb-4 text-blue-400">下游:品牌服饰与家纺</h3>
<div class="space-y-3">
<div class="bg-gray-700 rounded p-3">
<div class="font-semibold">波司登</div>
<div class="text-sm text-gray-400">全球羽绒服专家</div>
<div class="text-sm text-gray-400">品牌定价权最强</div>
</div>
<div class="bg-gray-700 rounded p-3">
<div class="font-semibold">富安娜/罗莱生活</div>
<div class="text-sm text-gray-400">家纺龙头</div>
<div class="text-sm text-gray-400">羽绒被渗透率提升</div>
</div>
<div class="bg-gray-700 rounded p-3">
<div class="font-semibold">安踏/李宁</div>
<div class="text-sm text-gray-400">运动品牌</div>
<div class="text-sm text-gray-400">冬季羽绒品类增量</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Core Companies Comparison -->
<section class="py-20 bg-gray-800">
<div class="container mx-auto px-6">
<h2 class="text-4xl md:text-5xl font-bold text-center mb-16 gradient-text">核心玩家四维对比</h2>
<div class="overflow-x-auto custom-scrollbar">
<table class="w-full bg-gray-900 rounded-lg overflow-hidden">
<thead>
<tr class="bg-purple-600">
<th class="px-6 py-4 text-left">公司</th>
<th class="px-6 py-4 text-center">业务纯度</th>
<th class="px-6 py-4 text-center">定价权</th>
<th class="px-6 py-4 text-center">资金实力</th>
<th class="px-6 py-4 text-center">风险敞口</th>
<th class="px-6 py-4 text-center">核心逻辑</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-700 hover:bg-gray-800">
<td class="px-6 py-4 font-semibold">古麒绒材</td>
<td class="px-6 py-4 text-center">⭐⭐⭐⭐⭐<br><span class="text-sm text-gray-400">99%羽绒业务</span></td>
<td class="px-6 py-4 text-center">⭐⭐⭐<br><span class="text-sm text-gray-400">技术溢价但账期被动</span></td>
<td class="px-6 py-4 text-center">⭐⭐⭐<br><span class="text-sm text-gray-400">IPO募资5亿</span></td>
<td class="px-6 py-4 text-center">⭐⭐⭐⭐⭐<br><span class="text-sm text-gray-400">原材料占比96%+</span></td>
<td class="px-6 py-4 text-sm">技术龙头+产能扩张<br><span class="text-red-400">需警惕现金流恶化</span></td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-800">
<td class="px-6 py-4 font-semibold">华英农业</td>
<td class="px-6 py-4 text-center">⭐⭐⭐⭐<br><span class="text-sm text-gray-400">70%羽绒+30%食品</span></td>
<td class="px-6 py-4 text-center">⭐⭐<br><span class="text-sm text-gray-400">加工费模式毛利率7-9%</span></td>
<td class="px-6 py-4 text-center">⭐⭐<br><span class="text-sm text-gray-400">依赖融资</span></td>
<td class="px-6 py-4 text-center">⭐⭐⭐⭐<br><span class="text-sm text-gray-400">订单锁定+库存跌价</span></td>
<td class="px-6 py-4 text-sm">规模优势+出清受益<br><span class="text-yellow-400">但盈利能力脆弱</span></td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-800">
<td class="px-6 py-4 font-semibold">波司登</td>
<td class="px-6 py-4 text-center">⭐⭐⭐<br><span class="text-sm text-gray-400">主品牌+多元化</span></td>
<td class="px-6 py-4 text-center">⭐⭐⭐⭐⭐<br><span class="text-sm text-gray-400">品牌溢价,可提价传导</span></td>
<td class="px-6 py-4 text-center">⭐⭐⭐⭐<br><span class="text-sm text-gray-400">港股龙头,现金充裕</span></td>
<td class="px-6 py-4 text-center">⭐⭐<br><span class="text-sm text-gray-400">成本占比<45%</span></td>
<td class="px-6 py-4 text-sm">品牌护城河最深<br><span class="text-green-400">估值已反映预期</span></td>
</tr>
<tr class="hover:bg-gray-800">
<td class="px-6 py-4 font-semibold">益客食品</td>
<td class="px-6 py-4 text-center">⭐⭐<br><span class="text-sm text-gray-400">羽绒为辅,食品为主</span></td>
<td class="px-6 py-4 text-center"><br><span class="text-sm text-gray-400">副产品,无单独定价</span></td>
<td class="px-6 py-4 text-center">⭐⭐⭐<br><span class="text-sm text-gray-400">肉鸭龙头</span></td>
<td class="px-6 py-4 text-center"><br><span class="text-sm text-gray-400">成本内部化</span></td>
<td class="px-6 py-4 text-sm">上游资源隐形标的<br><span class="text-blue-400">业务纯度不足</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Stock Data Section -->
<section class="py-20 bg-gray-900">
<div class="container mx-auto px-6">
<h2 class="text-4xl md:text-5xl font-bold text-center mb-16 gradient-text">相关股票数据</h2>
<div class="overflow-x-auto custom-scrollbar">
<table class="w-full bg-gray-800 rounded-lg overflow-hidden text-sm">
<thead>
<tr class="bg-gradient-to-r from-purple-600 to-purple-700">
<th class="px-4 py-3 text-left">股票名称</th>
<th class="px-4 py-3 text-left">分类</th>
<th class="px-4 py-3 text-left">行业</th>
<th class="px-4 py-3 text-left">项目</th>
<th class="px-4 py-3 text-left">产业链/地位</th>
<th class="px-4 py-3 text-left">关键数据</th>
<th class="px-4 py-3 text-left">投资逻辑</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-700 hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 font-semibold text-purple-400">古麒绒材</td>
<td class="px-4 py-3">相关业务</td>
<td class="px-4 py-3">羽绒产品</td>
<td class="px-4 py-3">高规格羽绒产品研产销</td>
<td class="px-4 py-3">原材料-研发-生产-销售</td>
<td class="px-4 py-3">营收占比99.27%2024年营收9.59亿</td>
<td class="px-4 py-3">A股唯一纯羽绒材料标的技术龙头</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 font-semibold text-purple-400">华英农业</td>
<td class="px-4 py-3">相关业务</td>
<td class="px-4 py-3">羽绒产品</td>
<td class="px-4 py-3">羽绒加工</td>
<td class="px-4 py-3">中国羽绒行业出口十强</td>
<td class="px-4 py-3">营收占比67.22%2024年营收31.8亿</td>
<td class="px-4 py-3">规模优势市占率国内15%,军工订单</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 font-semibold text-purple-400">益客食品</td>
<td class="px-4 py-3">相关业务</td>
<td class="px-4 py-3">羽绒产品</td>
<td class="px-4 py-3">原料绒生产</td>
<td class="px-4 py-3">原料绒-毛梗/毛片副产品</td>
<td class="px-4 py-3">2024年羽绒营收9.19亿元</td>
<td class="px-4 py-3">上游资源隐形标的年屠宰3.5亿只</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 font-semibold text-purple-400">立华股份</td>
<td class="px-4 py-3">相关业务</td>
<td class="px-4 py-3">养鹅业务</td>
<td class="px-4 py-3">种鹅繁育/商品鹅养殖</td>
<td class="px-4 py-3">育种-种鹅繁育-商品鹅养殖及销售</td>
<td class="px-4 py-3">2024年销售商品鹅209.63万只,同比+21.73%</td>
<td class="px-4 py-3">鹅绒高端原料,羽毛制作羽毛球</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 font-semibold text-purple-400">煌上煌</td>
<td class="px-4 py-3">相关业务</td>
<td class="px-4 py-3">羽绒加工</td>
<td class="px-4 py-3">羽绒加工</td>
<td class="px-4 py-3">子公司加工</td>
<td class="px-4 py-3">持股51%子公司丰城煌鹏羽绒</td>
<td class="px-4 py-3">业务占比较小,弹性有限</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 font-semibold text-blue-400">森马服饰</td>
<td class="px-4 py-3">羽绒服</td>
<td class="px-4 py-3">羽绒服</td>
<td class="px-4 py-3">品牌运营</td>
<td class="px-4 py-3">品牌设计-生产-销售</td>
<td class="px-4 py-3">古麒绒材大客户占比约20%</td>
<td class="px-4 py-3">下游品牌商,拥有定价权</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 font-semibold text-blue-400">海澜之家</td>
<td class="px-4 py-3">羽绒服</td>
<td class="px-4 py-3">羽绒服</td>
<td class="px-4 py-3">品牌运营</td>
<td class="px-4 py-3">品牌设计-生产-销售</td>
<td class="px-4 py-3">古麒绒材大客户占比约20%</td>
<td class="px-4 py-3">下游品牌商,拥有定价权</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 font-semibold text-blue-400">探路者</td>
<td class="px-4 py-3">羽绒服</td>
<td class="px-4 py-3">羽绒服</td>
<td class="px-4 py-3">户外服装</td>
<td class="px-4 py-3">户外品牌-羽绒服产品</td>
<td class="px-4 py-3">专业户外羽绒服市场</td>
<td class="px-4 py-3">户外领域羽绒服,细分市场龙头</td>
</tr>
<tr class="border-b border-gray-700 hover:bg-gray-700 transition-colors">
<td class="px-4 py-3 font-semibold text-blue-400">嘉曼服饰</td>
<td class="px-4 py-3">羽绒服</td>
<td class="px-4 py-3">羽绒服</td>
<td class="px-4 py-3">品牌运营</td>
<td class="px-4 py-3">品牌设计-生产-销售</td>
<td class="px-4 py-3">童装羽绒服市场</td>
<td class="px-4 py-3">儿童羽绒服细分市场</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Investment Advice Section -->
<section class="py-20 bg-gray-800">
<div class="container mx-auto px-6">
<h2 class="text-4xl md:text-5xl font-bold text-center mb-16 gradient-text">投资建议与风险提示</h2>
<div class="grid md:grid-cols-2 gap-8">
<div class="glass-effect rounded-xl p-8">
<h3 class="text-2xl font-bold mb-6 text-green-400">投资优先级</h3>
<div class="space-y-4">
<div class="flex items-center space-x-3">
<span class="text-3xl">🥇</span>
<div>
<div class="font-semibold">超配:波司登、益客食品</div>
<div class="text-sm text-gray-400">品牌护城河+上游资源</div>
</div>
</div>
<div class="flex items-center space-x-3">
<span class="text-3xl">🥈</span>
<div>
<div class="font-semibold">标配:华英农业</div>
<div class="text-sm text-gray-400">规模优势+军工订单</div>
</div>
</div>
<div class="flex items-center space-x-3">
<span class="text-3xl">🥉</span>
<div>
<div class="font-semibold">低配:古麒绒材、富安娜</div>
<div class="text-sm text-gray-400">情绪溢价高、逻辑不纯</div>
</div>
</div>
</div>
</div>
<div class="glass-effect rounded-xl p-8">
<h3 class="text-2xl font-bold mb-6 text-red-400">关键风险</h3>
<div class="space-y-4">
<div class="flex items-start space-x-3">
<i class="fas fa-exclamation-triangle text-yellow-400 mt-1"></i>
<div>
<div class="font-semibold">价格数据混乱</div>
<div class="text-sm text-gray-400">新闻涨幅241% vs 实际涨幅53%</div>
</div>
</div>
<div class="flex items-start space-x-3">
<i class="fas fa-exclamation-triangle text-yellow-400 mt-1"></i>
<div>
<div class="font-semibold">中游毛利率压缩</div>
<div class="text-sm text-gray-400">订单锁定导致毛利率仅7-9%</div>
</div>
</div>
<div class="flex items-start space-x-3">
<i class="fas fa-exclamation-triangle text-yellow-400 mt-1"></i>
<div>
<div class="font-semibold">库存周期错配</div>
<div class="text-sm text-gray-400">囤货压力与资金占用</div>
</div>
</div>
<div class="flex items-start space-x-3">
<i class="fas fa-exclamation-triangle text-yellow-400 mt-1"></i>
<div>
<div class="font-semibold">暖冬风险</div>
<div class="text-sm text-gray-400">2025年Q1业绩证伪概率高</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="py-12 bg-gray-900 border-t border-gray-800">
<div class="container mx-auto px-6 text-center">
<p class="text-gray-400 mb-4">本报告仅供参考,不构成投资建议</p>
<p class="text-sm text-gray-500">数据来源:公开新闻、路演记录、研报 | 更新时间2025年11月</p>
</div>
</footer>
<script>
// Smooth scroll function
function scrollToSection(sectionId) {
const section = document.getElementById(sectionId);
section.scrollIntoView({ behavior: 'smooth' });
}
// Add animation on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -100px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('price-up');
}
});
}, observerOptions);
document.querySelectorAll('.card-hover').forEach(el => {
observer.observe(el);
});
// Dynamic date update
const updateDate = () => {
const now = new Date();
const options = { year: 'numeric', month: 'long', day: 'numeric' };
document.getElementById('currentDate').textContent = now.toLocaleDateString('zh-CN', options);
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
updateDate();
// Add hover effect to table rows
const tableRows = document.querySelectorAll('tbody tr');
tableRows.forEach(row => {
row.addEventListener('mouseenter', function() {
this.style.transform = 'scale(1.02)';
this.style.transition = 'transform 0.2s ease';
});
row.addEventListener('mouseleave', function() {
this.style.transform = 'scale(1)';
});
});
});
// Parallax effect for hero section
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset;
const hero = document.querySelector('.gradient-bg');
if (hero) {
hero.style.transform = `translateY(${scrolled * 0.5}px)`;
}
});
</script>
</body>
</html>

576
public/htmls/谷歌.html Normal file
View File

@@ -0,0 +1,576 @@
我将为您创建一个专业的金融科技风格HTML页面展示谷歌AI转型的完整投资分析。这个页面将融合数据可视化、动态效果和响应式设计。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>谷歌AI产业链投资分析 - 战略蜕变与价值重估</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
* {
font-family: 'Inter', sans-serif;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.pulse-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.7;
}
}
.number-animate {
animation: countUp 1s ease-out;
}
@keyframes countUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.timeline-item {
position: relative;
padding-left: 40px;
}
.timeline-item::before {
content: '';
position: absolute;
left: 0;
top: 8px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.timeline-item::after {
content: '';
position: absolute;
left: 5px;
top: 20px;
width: 2px;
height: calc(100% + 10px);
background: #e5e7eb;
}
.timeline-item:last-child::after {
display: none;
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.glow-text {
text-shadow: 0 0 20px rgba(102, 126, 234, 0.5);
}
.scroll-hidden::-webkit-scrollbar {
display: none;
}
.badge-glow {
animation: badgeGlow 2s ease-in-out infinite alternate;
}
@keyframes badgeGlow {
from {
box-shadow: 0 0 5px rgba(102, 126, 234, 0.5);
}
to {
box-shadow: 0 0 20px rgba(102, 126, 234, 0.8);
}
}
</style>
</head>
<body class="bg-gray-50">
<!-- Hero Section -->
<section class="gradient-bg text-white py-20 px-4">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-10">
<div class="inline-flex items-center justify-center w-16 h-16 bg-white/20 rounded-full mb-6 badge-glow">
<i class="fab fa-google text-3xl"></i>
</div>
<h1 class="text-5xl md:text-6xl font-bold mb-6 glow-text">
谷歌AI产业链投资分析
</h1>
<p class="text-xl md:text-2xl text-blue-100 max-w-3xl mx-auto">
从"搜索广告公司"向"AI基础设施+应用生态平台"的战略蜕变
</p>
</div>
<div class="grid md:grid-cols-4 gap-6 mt-16">
<div class="glass-effect rounded-xl p-6 card-hover">
<div class="text-3xl font-bold number-animate">450<span class="text-xl"></span></div>
<div class="text-blue-200 mt-2">等效H100算力</div>
<div class="text-sm text-blue-300 mt-1">2025年预期</div>
</div>
<div class="glass-effect rounded-xl p-6 card-hover">
<div class="text-3xl font-bold number-animate">1300<span class="text-xl">万亿</span></div>
<div class="text-blue-200 mt-2">月均Token处理量</div>
<div class="text-sm text-blue-300 mt-1">环比增长30%</div>
</div>
<div class="glass-effect rounded-xl p-6 card-hover">
<div class="text-3xl font-bold number-animate">23.7<span class="text-xl">%</span></div>
<div class="text-blue-200 mt-2">云业务利润率</div>
<div class="text-sm text-blue-300 mt-1">Q3 2025</div>
</div>
<div class="glass-effect rounded-xl p-6 card-hover">
<div class="text-3xl font-bold number-animate">1550<span class="text-xl">亿美元</span></div>
<div class="text-blue-200 mt-2">云积压订单</div>
<div class="text-sm text-blue-300 mt-1">环比增长46.2%</div>
</div>
</div>
</div>
</section>
<!-- Key Timeline -->
<section class="py-16 px-4 bg-white">
<div class="max-w-7xl mx-auto">
<h2 class="text-3xl font-bold text-gray-800 mb-8 text-center">
<i class="fas fa-clock text-purple-600 mr-3"></i>关键时间轴
</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="timeline-item">
<div class="text-sm text-purple-600 font-semibold mb-2">2024年2-3月</div>
<div class="text-lg font-semibold text-gray-800 mb-2">AI Overview大规模推广</div>
<div class="text-gray-600 text-sm">Token消耗量达480万亿/月</div>
</div>
<div class="timeline-item">
<div class="text-sm text-purple-600 font-semibold mb-2">2024年5月</div>
<div class="text-lg font-semibold text-gray-800 mb-2">Gemini 2.5 Pro发布</div>
<div class="text-gray-600 text-sm">AI Mode上线月活破1亿</div>
</div>
<div class="timeline-item">
<div class="text-sm text-purple-600 font-semibold mb-2">2024年8月</div>
<div class="text-lg font-semibold text-gray-800 mb-2">Pixel 9搭载Gemini Nano</div>
<div class="text-gray-600 text-sm">Gemini Live对标Apple Intelligence</div>
</div>
<div class="timeline-item">
<div class="text-sm text-purple-600 font-semibold mb-2">2024年9月</div>
<div class="text-lg font-semibold text-gray-800 mb-2">反垄断判决超预期</div>
<div class="text-gray-600 text-sm">保留Chrome消除不确定性</div>
</div>
<div class="timeline-item">
<div class="text-sm text-purple-600 font-semibold mb-2">2024年10月</div>
<div class="text-lg font-semibold text-gray-800 mb-2">Q3财报验证AI正循环</div>
<div class="text-gray-600 text-sm">搜索收入增速创2-3年新高</div>
</div>
<div class="timeline-item">
<div class="text-sm text-purple-600 font-semibold mb-2">2024年11月</div>
<div class="text-lg font-semibold text-gray-800 mb-2">TPU外供重大突破</div>
<div class="text-gray-600 text-sm">OpenAI/META成为客户</div>
</div>
</div>
</div>
</section>
<!-- Core Logic Section -->
<section class="py-16 px-4 bg-gray-100">
<div class="max-w-7xl mx-auto">
<h2 class="text-3xl font-bold text-gray-800 mb-12 text-center">
<i class="fas fa-brain text-purple-600 mr-3"></i>核心逻辑分析
</h2>
<div class="grid md:grid-cols-3 gap-8 mb-12">
<div class="bg-white rounded-2xl p-8 card-hover shadow-lg">
<div class="w-14 h-14 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center mb-6">
<i class="fas fa-microchip text-white text-2xl"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-4">算力自主化</h3>
<p class="text-gray-600 leading-relaxed">
自研TPU芯片性能对标英伟达B200凭借功耗和成本优势云业务利润率从11%提升至23.7%单季度利润率提升超12个百分点
</p>
<div class="mt-6 flex items-center text-purple-600 font-semibold">
<span>450万等效H100</span>
<i class="fas fa-arrow-trend-up ml-2"></i>
</div>
</div>
<div class="bg-white rounded-2xl p-8 card-hover shadow-lg">
<div class="w-14 h-14 bg-gradient-to-br from-green-500 to-teal-600 rounded-full flex items-center justify-center mb-6">
<i class="fas fa-robot text-white text-2xl"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-4">模型代际领先</h3>
<p class="text-gray-600 leading-relaxed">
Gemini 2.5 Pro在LMArena测评中ELO分数达1448分Veo 3、Imagen 4、Genie模型在多模态领域实现突破性进展
</p>
<div class="mt-6 flex items-center text-green-600 font-semibold">
<span>多模态原生能力</span>
<i class="fas fa-trophy ml-2"></i>
</div>
</div>
<div class="bg-white rounded-2xl p-8 card-hover shadow-lg">
<div class="w-14 h-14 bg-gradient-to-br from-orange-500 to-red-600 rounded-full flex items-center justify-center mb-6">
<i class="fas fa-chart-line text-white text-2xl"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-4">流量护城河转化</h3>
<p class="text-gray-600 leading-relaxed">
AI Overview覆盖15-20亿月活AI Mode Token消耗是传统模式的2-3倍重构搜索商业模式提升广告ROI
</p>
<div class="mt-6 flex items-center text-orange-600 font-semibold">
<span>20亿月活用户</span>
<i class="fas fa-users ml-2"></i>
</div>
</div>
</div>
<!-- Market Sentiment -->
<div class="bg-gradient-to-r from-purple-50 to-blue-50 rounded-2xl p-8">
<h3 class="text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-chart-pie text-purple-600 mr-3"></i>市场预期差
</h3>
<div class="grid md:grid-cols-2 gap-8">
<div>
<h4 class="font-semibold text-gray-700 mb-4">当前市场认知</h4>
<ul class="space-y-3">
<li class="flex items-start">
<span class="text-red-500 mr-2"><i class="fas fa-times-circle"></i></span>
<span class="text-gray-600">估值便宜但增长乏力PE 18倍</span>
</li>
<li class="flex items-start">
<span class="text-red-500 mr-2"><i class="fas fa-times-circle"></i></span>
<span class="text-gray-600">担忧AI搜索冲击广告收入</span>
</li>
<li class="flex items-start">
<span class="text-red-500 mr-2"><i class="fas fa-times-circle"></i></span>
<span class="text-gray-600">机构低配,情绪偏谨慎</span>
</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-gray-700 mb-4">产业实际进展</h4>
<ul class="space-y-3">
<li class="flex items-start">
<span class="text-green-500 mr-2"><i class="fas fa-check-circle"></i></span>
<span class="text-gray-600">产业链订单上修30-50%</span>
</li>
<li class="flex items-start">
<span class="text-green-500 mr-2"><i class="fas fa-check-circle"></i></span>
<span class="text-gray-600">TPU外供打开第二增长曲线</span>
</li>
<li class="flex items-start">
<span class="text-green-500 mr-2"><i class="fas fa-check-circle"></i></span>
<span class="text-gray-600">进入"业绩饥渴"状态</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- Stock Chain Table -->
<section class="py-16 px-4 bg-white">
<div class="max-w-7xl mx-auto">
<h2 class="text-3xl font-bold text-gray-800 mb-8 text-center">
<i class="fas fa-network-wired text-purple-600 mr-3"></i>谷歌产业链核心标的
</h2>
<div class="mb-8 flex flex-wrap gap-4 justify-center">
<button onclick="filterCategory('all')" class="px-6 py-2 bg-purple-600 text-white rounded-full hover:bg-purple-700 transition">
全部
</button>
<button onclick="filterCategory('光模块')" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-full hover:bg-gray-300 transition">
光模块
</button>
<button onclick="filterCategory('PCB')" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-full hover:bg-gray-300 transition">
PCB
</button>
<button onclick="filterCategory('OCS')" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-full hover:bg-gray-300 transition">
OCS
</button>
<button onclick="filterCategory('液冷散热')" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-full hover:bg-gray-300 transition">
液冷散热
</button>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse" id="stockTable">
<thead>
<tr class="bg-gradient-to-r from-purple-600 to-blue-600 text-white">
<th class="p-4 text-left font-semibold">分类</th>
<th class="p-4 text-left font-semibold">个股</th>
<th class="p-4 text-left font-semibold">相关性描述</th>
<th class="p-4 text-left font-semibold">消息来源</th>
<th class="p-4 text-center font-semibold">投资评级</th>
</tr>
</thead>
<tbody id="stockTableBody">
<!-- Table content will be dynamically generated -->
</tbody>
</table>
</div>
</div>
</section>
<!-- Risk Analysis -->
<section class="py-16 px-4 bg-gradient-to-br from-red-50 to-orange-50">
<div class="max-w-7xl mx-auto">
<h2 class="text-3xl font-bold text-gray-800 mb-12 text-center">
<i class="fas fa-exclamation-triangle text-red-600 mr-3"></i>风险提示
</h2>
<div class="grid md:grid-cols-3 gap-8">
<div class="bg-white rounded-xl p-6 border-l-4 border-red-500">
<h3 class="text-xl font-bold text-gray-800 mb-4">
<i class="fas fa-microchip text-red-500 mr-2"></i>技术风险
</h3>
<ul class="space-y-2 text-gray-600">
<li>• AI Mode幻觉率达15-20%</li>
<li>• Agent识别第三方应用仅30%</li>
<li>• Agent to Agent协议推进缓慢</li>
</ul>
</div>
<div class="bg-white rounded-xl p-6 border-l-4 border-orange-500">
<h3 class="text-xl font-bold text-gray-800 mb-4">
<i class="fas fa-dollar-sign text-orange-500 mr-2"></i>商业化风险
</h3>
<ul class="space-y-2 text-gray-600">
<li>• AI Ultra月费245美元过高</li>
<li>• 广告展示次数或降30-40%</li>
<li>• Gemini Advanced仅3000万用户</li>
</ul>
</div>
<div class="bg-white rounded-xl p-6 border-l-4 border-yellow-500">
<h3 class="text-xl font-bold text-gray-800 mb-4">
<i class="fas fa-gavel text-yellow-600 mr-2"></i>政策风险
</h3>
<ul class="space-y-2 text-gray-600">
<li>• 美国司法部终极裁决未定</li>
<li>• 欧盟数字税反制风险</li>
<li>• 广告业务拆分风险犹存</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Investment Advice -->
<section class="py-16 px-4 gradient-bg text-white">
<div class="max-w-7xl mx-auto">
<h2 class="text-3xl font-bold mb-12 text-center glow-text">
<i class="fas fa-lightbulb mr-3"></i>投资建议
</h2>
<div class="grid md:grid-cols-2 gap-8">
<div class="glass-effect rounded-2xl p-8">
<h3 class="text-2xl font-bold mb-6">谷歌本体 (GOOGL)</h3>
<div class="mb-6">
<div class="text-4xl font-bold mb-2">增持评级</div>
<div class="text-blue-200">目标价:$233-250</div>
</div>
<ul class="space-y-3 text-blue-100">
<li class="flex items-start">
<i class="fas fa-check-circle mr-3 mt-1"></i>
<span>当前18倍PE未反映TPU外供价值</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle mr-3 mt-1"></i>
<span>监管出清后估值向25倍PE修复</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle mr-3 mt-1"></i>
<span>AI订阅带来第二增长曲线</span>
</li>
</ul>
</div>
<div class="glass-effect rounded-2xl p-8">
<h3 class="text-2xl font-bold mb-6">A股产业链配置</h3>
<div class="mb-6">
<div class="text-4xl font-bold mb-2">重点配置</div>
<div class="text-blue-200">硬件链业绩确定性最强</div>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between bg-white/10 rounded-lg p-3">
<span>光库科技</span>
<span class="text-green-300 font-semibold">第一优先级</span>
</div>
<div class="flex items-center justify-between bg-white/10 rounded-lg p-3">
<span>中际旭创</span>
<span class="text-green-300 font-semibold">第二优先级</span>
</div>
<div class="flex items-center justify-between bg-white/10 rounded-lg p-3">
<span>沪电股份</span>
<span class="text-green-300 font-semibold">第三优先级</span>
</div>
</div>
</div>
</div>
<div class="mt-12 text-center">
<div class="inline-flex items-center bg-white/20 rounded-full px-8 py-4">
<i class="fas fa-chart-line text-3xl mr-4 pulse-dot"></i>
<div class="text-left">
<div class="text-sm text-blue-200">关键跟踪指标</div>
<div class="text-xl font-bold">Q4云业务利润率 & Token环比增速</div>
</div>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-gray-900 text-white py-8 px-4">
<div class="max-w-7xl mx-auto text-center">
<p class="text-gray-400">© 2024 谷歌AI产业链投资分析 | 数据来源:公开研报、路演纪要、产业链调研</p>
<p class="text-gray-500 text-sm mt-2">风险提示:投资有风险,入市需谨慎</p>
</div>
</footer>
<script>
// Stock data processing
const stockData = [
{category: '光模块', name: '中际旭创', desc: '供货谷歌800G光模块份额A股第一2025年谷歌光模块采购量约350万只中际旭创占其采购份额的70%', source: '网传纪要', rating: '强烈推荐'},
{category: '光模块', name: '新易盛', desc: '供货谷歌800G光模块份额A股第二2025年首次切入谷歌800G供应链', source: '网传纪要', rating: '推荐'},
{category: '光模块', name: '长芯盛创', desc: '800G硅光模块已通过谷歌多轮验证计划2025年第三季度量产预计年出货量超50万只', source: '网传纪要', rating: '推荐'},
{category: 'PCB', name: '沪电股份', desc: '供货谷歌PCB份额A股第一谷歌TPU PCB的核心供应商在谷歌TPU供应商中占比约30%', source: '网传纪要', rating: '强烈推荐'},
{category: 'PCB', name: '胜宏科技', desc: '谷歌V7大份额一供', source: '机构研报', rating: '推荐'},
{category: 'OCS', name: '腾景科技', desc: '谷歌OCS交换机的核心光学器件供应商,业务收入占比已达28%', source: '公开资料', rating: '推荐'},
{category: 'OCS', name: '光库科技', desc: '2025年6月份并购的武汉捷普工厂是过去谷歌OCS交换机方案的独家代工厂商', source: '网传纪要', rating: '强烈推荐'},
{category: '液冷散热', name: '英维克', desc: '公司推出基于谷歌Descartes 5 CDU规格的CDU产品同时展示26MW全链路液冷案例', source: '公开资料', rating: '推荐'}
];
// Populate table
function populateTable(data) {
const tbody = document.getElementById('stockTableBody');
tbody.innerHTML = '';
data.forEach((stock, index) => {
const row = document.createElement('tr');
row.className = 'border-b hover:bg-gray-50 transition-colors';
row.innerHTML = `
<td class="p-4">
<span class="inline-block px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm font-medium">
${stock.category}
</span>
</td>
<td class="p-4 font-semibold text-gray-800">${stock.name}</td>
<td class="p-4 text-gray-600 max-w-md">${stock.desc}</td>
<td class="p-4 text-gray-500 text-sm">${stock.source}</td>
<td class="p-4 text-center">
<span class="inline-block px-3 py-1 ${
stock.rating === '强烈推荐' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'
} rounded-full text-sm font-medium">
${stock.rating}
</span>
</td>
`;
tbody.appendChild(row);
});
}
// Filter category
function filterCategory(category) {
const buttons = document.querySelectorAll('button');
buttons.forEach(btn => {
if (btn.textContent === '全部' && category === 'all' ||
btn.textContent === category) {
btn.className = 'px-6 py-2 bg-purple-600 text-white rounded-full hover:bg-purple-700 transition';
} else if (btn.textContent !== '全部') {
btn.className = 'px-6 py-2 bg-gray-200 text-gray-700 rounded-full hover:bg-gray-300 transition';
}
});
const filtered = category === 'all' ? stockData : stockData.filter(s => s.category === category);
populateTable(filtered);
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
populateTable(stockData);
// Add smooth scroll
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
// Animate numbers on scroll
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('number-animate');
}
});
});
document.querySelectorAll('.number-animate').forEach(el => {
observer.observe(el);
});
});
</script>
</body>
</html>
这个HTML页面完整展示了谷歌AI转型的投资分析包含以下核心特性
## 🎯 核心亮点
1. **动态数据可视化**Hero区域展示关键数据指标带动画效果
2. **时间轴展示**清晰呈现谷歌AI发展的关键节点
3. **产业链分析**:交互式表格展示核心标的,支持分类筛选
4. **风险提示矩阵**:三维度风险分析(技术、商业化、政策)
5. **投资建议卡片**:玻璃拟态设计展示具体配置建议
## 💫 设计特色
- **渐变色彩系统**:紫色到蓝色的科技感渐变
- **卡片悬浮效果**鼠标悬停时的3D浮起动画
- **脉冲动画**:重要指标的呼吸灯效果
- **响应式布局**:完美适配移动端和桌面端
- **微交互设计**:按钮点击、表格筛选等细节动画
## 📊 数据展示
- 完整保留了insight中的核心数据和逻辑
- 股票产业链表格支持动态筛选
- 关键数据突出显示450万算力、1300万亿Token等
- 风险和投资建议分层展示
这个页面不仅是一个信息展示工具更是一个专业的投资决策辅助界面通过视觉化设计帮助投资者快速把握谷歌AI产业链的投资机会。

View File

@@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.12.0'
const PACKAGE_VERSION = '2.12.2'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View File

@@ -1,3 +0,0 @@
INFO Accepting connections at http://localhost:58321
INFO Gracefully shutting down. Please wait...

View File

@@ -53,6 +53,21 @@ const BytedeskWidget = ({
widgetRef.current = bytedesk;
console.log('[Bytedesk] Widget初始化成功');
// ⚡ 屏蔽 STOMP WebSocket 错误日志(不影响功能)
// Bytedesk SDK 内部的 /stomp WebSocket 连接失败不影响核心客服功能
// SDK 会自动降级使用 HTTP 轮询
const originalConsoleError = console.error;
console.error = function(...args) {
const errorMsg = args.join(' ');
// 忽略 /stomp 和 STOMP 相关错误
if (errorMsg.includes('/stomp') ||
errorMsg.includes('stomp onWebSocketError') ||
(errorMsg.includes('WebSocket connection to') && errorMsg.includes('/stomp'))) {
return; // 不输出日志
}
originalConsoleError.apply(console, args);
};
if (onLoad) {
onLoad(bytedesk);
}
@@ -78,26 +93,43 @@ const BytedeskWidget = ({
document.body.appendChild(script);
scriptRef.current = script;
// 清理函数
// 清理函数 - 增强错误处理,防止 React 18 StrictMode 双重清理报错
return () => {
console.log('[Bytedesk] 清理Widget');
// 移除脚本
if (scriptRef.current && document.body.contains(scriptRef.current)) {
document.body.removeChild(scriptRef.current);
try {
if (scriptRef.current && scriptRef.current.parentNode) {
scriptRef.current.parentNode.removeChild(scriptRef.current);
}
scriptRef.current = null;
} catch (error) {
console.warn('[Bytedesk] 移除脚本失败(可能已被移除):', error.message);
}
// 移除Widget DOM元素
const widgetElements = document.querySelectorAll('[class*="bytedesk"], [id*="bytedesk"]');
widgetElements.forEach(el => {
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}
});
try {
const widgetElements = document.querySelectorAll('[class*="bytedesk"], [id*="bytedesk"]');
widgetElements.forEach(el => {
try {
if (el && el.parentNode && el.parentNode.contains(el)) {
el.parentNode.removeChild(el);
}
} catch (err) {
// 忽略单个元素移除失败(可能已被移除)
}
});
} catch (error) {
console.warn('[Bytedesk] 清理Widget DOM元素失败:', error.message);
}
// 清理全局对象
if (window.BytedeskWeb) {
delete window.BytedeskWeb;
try {
if (window.BytedeskWeb) {
delete window.BytedeskWeb;
}
} catch (error) {
console.warn('[Bytedesk] 清理全局对象失败:', error.message);
}
};
}, [config, autoLoad, onLoad, onError]);

View File

@@ -44,9 +44,9 @@ export const bytedeskConfig = {
// 聊天配置(必需)
chatConfig: {
org: df_org_uid, // 组织ID
org: 'df_org_uid', // 组织ID
t: '1', // 类型: 1=人工客服, 2=机器人
sid: df_wg_uid, // 工作组ID
sid: 'df_wg_uid', // 工作组ID
},
};

View File

@@ -85,13 +85,10 @@ export default function AuthFormContent() {
const [showNicknamePrompt, setShowNicknamePrompt] = useState(false);
const [currentPhone, setCurrentPhone] = useState("");
// 响应式布局配置
const isMobile = useBreakpointValue({ base: true, md: false });
// 事件追踪
const authEvents = useAuthEvents({
component: 'AuthFormContent',
isMobile: isMobile
isMobile,
});
const stackDirection = useBreakpointValue({ base: "column", md: "row" });
const stackSpacing = useBreakpointValue({ base: 4, md: 2 }); // ✅ 桌面端从32px减至8px更紧凑
@@ -186,8 +183,6 @@ export default function AuthFormContent() {
purpose: config.api.purpose
};
logger.api.request('POST', '/api/auth/send-verification-code', requestData);
const response = await fetch('/api/auth/send-verification-code', {
method: 'POST',
headers: {
@@ -203,8 +198,6 @@ export default function AuthFormContent() {
const data = await response.json();
logger.api.response('POST', '/api/auth/send-verification-code', response.status, data);
if (!isMountedRef.current) return;
if (!data) {
@@ -309,12 +302,6 @@ export default function AuthFormContent() {
login_type: 'phone',
};
logger.api.request('POST', '/api/auth/login-with-code', {
credential: cleanedPhone.substring(0, 3) + '****' + cleanedPhone.substring(7),
verification_code: verificationCode.substring(0, 2) + '****',
login_type: 'phone'
});
// 调用API根据模式选择不同的endpoint
const response = await fetch('/api/auth/login-with-code', {
method: 'POST',
@@ -331,11 +318,6 @@ export default function AuthFormContent() {
const data = await response.json();
logger.api.response('POST', '/api/auth/login-with-code', response.status, {
...data,
user: data.user ? { id: data.user.id, phone: data.user.phone } : null
});
if (!isMountedRef.current) return;
if (!data) {

View File

@@ -1,13 +1,7 @@
// src/components/Auth/AuthModalManager.js
import React, { useEffect, useRef } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalCloseButton,
useBreakpointValue
} from '@chakra-ui/react';
import { Modal } from 'antd';
import { useBreakpointValue } from '@chakra-ui/react';
import { useAuthModal } from '../../hooks/useAuthModal';
import AuthFormContent from './AuthFormContent';
import { trackEventAsync } from '@lib/posthog';
@@ -44,85 +38,43 @@ export default function AuthModalManager() {
}
}, [isAuthModalOpen]);
// 响应式尺寸配置
const modalSize = useBreakpointValue({
base: "md", // 移动端md不占满全屏
sm: "md", // 小屏md
md: "lg", // 屏:lg
lg: "xl" // 屏:xl更紧凑
});
// 响应式宽度配置
const modalMaxW = useBreakpointValue({
base: "90%", // 移动端屏幕宽度的90%
sm: "90%", // 小屏90%
md: "700px", // 中屏固定700px
lg: "700px" // 大屏固定700px
});
// 响应式水平边距
const modalMx = useBreakpointValue({
base: 4, // 移动端左右各16px边距
md: "auto" // 桌面端:自动居中
});
// 响应式垂直边距
const modalMy = useBreakpointValue({
base: 8, // 移动端上下各32px边距
md: 8 // 桌面端上下各32px边距
});
// 条件渲染:只在打开时才渲染 Modal避免创建不必要的 Portal
if (!isAuthModalOpen) {
return null;
}
// 响应式宽度配置Ant Design Modal 使用数字或字符串)
const modalMaxW = useBreakpointValue(
{
base: "90%", // 移动端屏幕宽度的90%
sm: "90%", // 屏:90%
md: "700px", // 屏:固定700px
lg: "700px" // 大屏固定700px
},
{ fallback: "700px", ssr: false }
);
// ✅ 使用 Ant Design Modal完全避开 Chakra UI Portal 的 AnimatePresence 问题
// Ant Design Modal 不使用 Framer Motion不会有 React 18 并发渲染的 insertBefore 错误
return (
<Modal
isOpen={isAuthModalOpen}
onClose={closeModal}
size={modalSize}
isCentered
closeOnOverlayClick={false} // 防止误点击背景关闭
closeOnEsc={true} // 允许ESC键关闭
scrollBehavior="inside" // 内容滚动
zIndex={999} // 低于导航栏(1000),不覆盖导航
open={isAuthModalOpen}
onCancel={closeModal}
footer={null}
width={modalMaxW}
centered
destroyOnHidden={true}
maskClosable={false}
keyboard={true}
zIndex={999}
styles={{
body: {
padding: '24px',
maxHeight: 'calc(90vh - 120px)',
overflowY: 'auto'
},
mask: {
backdropFilter: 'blur(10px)',
backgroundColor: 'rgba(0, 0, 0, 0.7)'
}
}}
>
{/* 半透明背景 + 模糊效果 */}
<ModalOverlay
bg="blackAlpha.700"
backdropFilter="blur(10px)"
/>
{/* 弹窗内容容器 */}
<ModalContent
bg="white"
boxShadow="2xl"
borderRadius="2xl"
maxW={modalMaxW}
mx={modalMx}
my={modalMy}
position="relative"
>
{/* 关闭按钮 */}
<ModalCloseButton
position="absolute"
right={4}
top={4}
zIndex={9999}
color="gray.500"
bg="transparent"
_hover={{ bg: "gray.100" }}
borderRadius="full"
size="lg"
onClick={closeModal}
/>
{/* 弹窗主体内容 */}
<ModalBody p={6}>
<AuthFormContent />
</ModalBody>
</ModalContent>
<AuthFormContent />
</Modal>
);
}

View File

@@ -23,8 +23,8 @@ import { FiTarget, FiCheckCircle, FiXCircle, FiClock, FiTool } from 'react-icons
* 执行计划卡片组件
*/
export const PlanCard = ({ plan, stepResults }) => {
const cardBg = useColorModeValue('blue.50', 'blue.900');
const borderColor = useColorModeValue('blue.200', 'blue.700');
const cardBg = useColorModeValue('blue.50', 'rgba(40, 45, 50, 0.8)');
const borderColor = useColorModeValue('blue.200', 'rgba(255, 215, 0, 0.3)');
const successColor = useColorModeValue('green.500', 'green.300');
const errorColor = useColorModeValue('red.500', 'red.300');
const pendingColor = useColorModeValue('gray.400', 'gray.500');
@@ -73,7 +73,7 @@ export const PlanCard = ({ plan, stepResults }) => {
<Icon as={FiTarget} color="blue.500" boxSize={5} />
<Text fontWeight="bold" fontSize="md">执行目标</Text>
</HStack>
<Text fontSize="sm" color="gray.600" pl={7}>
<Text fontSize="sm" color={useColorModeValue('gray.600', '#9BA1A6')} pl={7}>
{plan.goal}
</Text>
@@ -83,7 +83,7 @@ export const PlanCard = ({ plan, stepResults }) => {
{plan.reasoning && (
<>
<Text fontSize="sm" fontWeight="bold">规划思路</Text>
<Text fontSize="sm" color="gray.600">
<Text fontSize="sm" color={useColorModeValue('gray.600', '#9BA1A6')}>
{plan.reasoning}
</Text>
<Divider />
@@ -106,7 +106,7 @@ export const PlanCard = ({ plan, stepResults }) => {
<HStack
key={index}
p={2}
bg={useColorModeValue('white', 'gray.700')}
bg={useColorModeValue('white', 'rgba(50, 55, 60, 0.6)')}
borderRadius="md"
borderWidth="1px"
borderColor={stepColor}
@@ -129,7 +129,7 @@ export const PlanCard = ({ plan, stepResults }) => {
status === 'failed' ? '✗ 失败' : '⏳ 等待'}
</Badge>
</HStack>
<Text fontSize="xs" color="gray.600">
<Text fontSize="xs" color={useColorModeValue('gray.600', '#9BA1A6')}>
{step.reason}
</Text>
</VStack>

View File

@@ -23,8 +23,8 @@ import { FiChevronDown, FiChevronUp, FiCheckCircle, FiXCircle, FiClock, FiDataba
export const StepResultCard = ({ stepResult }) => {
const [isExpanded, setIsExpanded] = useState(false);
const cardBg = useColorModeValue('white', 'gray.700');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const cardBg = useColorModeValue('white', 'rgba(40, 45, 50, 0.8)');
const borderColor = useColorModeValue('gray.200', 'rgba(255, 215, 0, 0.2)');
const successColor = useColorModeValue('green.500', 'green.300');
const errorColor = useColorModeValue('red.500', 'red.300');
@@ -80,7 +80,7 @@ export const StepResultCard = ({ stepResult }) => {
justify="space-between"
cursor="pointer"
onClick={() => setIsExpanded(!isExpanded)}
_hover={{ bg: useColorModeValue('gray.50', 'gray.600') }}
_hover={{ bg: useColorModeValue('gray.50', 'rgba(50, 55, 60, 0.7)') }}
>
<HStack flex={1}>
<Icon as={StatusIcon} color={`${statusColorScheme}.500`} boxSize={5} />
@@ -94,7 +94,7 @@ export const StepResultCard = ({ stepResult }) => {
stepResult.status === 'failed' ? '失败' : '执行中'}
</Badge>
</HStack>
<Text fontSize="xs" color="gray.500">
<Text fontSize="xs" color={useColorModeValue('gray.500', '#9BA1A6')}>
耗时: {stepResult.execution_time?.toFixed(2)}s
</Text>
</VStack>
@@ -140,7 +140,7 @@ export const StepResultCard = ({ stepResult }) => {
maxH="300px"
overflowY="auto"
p={2}
bg={useColorModeValue('gray.50', 'gray.800')}
bg={useColorModeValue('gray.50', 'rgba(25, 28, 32, 0.6)')}
borderRadius="md"
fontSize="xs"
>
@@ -155,7 +155,7 @@ export const StepResultCard = ({ stepResult }) => {
</Code>
))}
{stepResult.result.length > 3 && (
<Text fontSize="xs" color="gray.500">
<Text fontSize="xs" color={useColorModeValue('gray.500', '#9BA1A6')}>
...还有 {stepResult.result.length - 3} 条记录
</Text>
)}
@@ -172,7 +172,7 @@ export const StepResultCard = ({ stepResult }) => {
{stepResult.status === 'failed' && stepResult.error && (
<VStack align="stretch" spacing={2}>
<Text fontSize="xs" fontWeight="bold" color="red.500">错误信息:</Text>
<Text fontSize="xs" color="red.600" p={2} bg="red.50" borderRadius="md">
<Text fontSize="xs" color={useColorModeValue('red.600', 'red.300')} p={2} bg={useColorModeValue('red.50', 'rgba(220, 38, 38, 0.15)')} borderRadius="md">
{stepResult.error}
</Text>
</VStack>

View File

@@ -1,7 +1,7 @@
// src/components/GlobalComponents.js
// 集中管理应用的全局组件
import React from 'react';
import React, { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useNotification } from '../contexts/NotificationContext';
import { logger } from '../utils/logger';
@@ -75,6 +75,9 @@ function ConnectionStatusBarWrapper() {
export function GlobalComponents() {
const location = useLocation();
// ✅ 缓存 Bytedesk 配置对象,避免每次渲染都创建新引用导致重新加载
const bytedeskConfigMemo = useMemo(() => getBytedeskConfig(), []);
return (
<>
{/* Socket 连接状态条 */}
@@ -89,9 +92,9 @@ export function GlobalComponents() {
{/* 通知容器 */}
<NotificationContainer />
{/* Bytedesk在线客服 - 根据路径条件性显示 */}
{/* Bytedesk在线客服 - 使用缓存的配置对象 */}
<BytedeskWidget
config={getBytedeskConfig()}
config={bytedeskConfigMemo}
autoLoad={true}
/>
</>

View File

@@ -0,0 +1,309 @@
/**
* 图片灯箱组件
* 点击图片放大查看
*/
import React, { useState } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalCloseButton,
Image,
Box,
IconButton,
HStack,
useDisclosure,
} from '@chakra-ui/react';
import { ChevronLeft, ChevronRight, X, ZoomIn } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
const MotionBox = motion(Box);
/**
* 单图片灯箱
*/
export const ImageLightbox = ({ src, alt, ...props }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
{/* 缩略图 */}
<Box
position="relative"
cursor="pointer"
onClick={onOpen}
_hover={{
'& .zoom-icon': {
opacity: 1,
},
}}
{...props}
>
<Image
src={src}
alt={alt}
w="100%"
h="100%"
objectFit="cover"
borderRadius="md"
transition="all 0.3s"
_hover={{
transform: 'scale(1.05)',
filter: 'brightness(0.8)',
}}
/>
{/* 放大图标 */}
<Box
className="zoom-icon"
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
opacity={0}
transition="opacity 0.3s"
pointerEvents="none"
>
<Box
bg="blackAlpha.700"
borderRadius="full"
p="3"
>
<ZoomIn size={32} color="white" />
</Box>
</Box>
</Box>
{/* 灯箱模态框 */}
<Modal isOpen={isOpen} onClose={onClose} size="full" isCentered>
<ModalOverlay bg="blackAlpha.900" backdropFilter="blur(10px)" />
<ModalContent bg="transparent" boxShadow="none">
<ModalCloseButton
position="fixed"
top="4"
right="4"
size="lg"
color="white"
bg="blackAlpha.600"
_hover={{ bg: 'blackAlpha.800' }}
borderRadius="full"
zIndex={2}
/>
<ModalBody display="flex" alignItems="center" justifyContent="center" p="0">
<MotionBox
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.3 }}
maxW="90vw"
maxH="90vh"
>
<Image
src={src}
alt={alt}
maxW="100%"
maxH="90vh"
objectFit="contain"
borderRadius="lg"
/>
</MotionBox>
</ModalBody>
</ModalContent>
</Modal>
</>
);
};
/**
* 多图片轮播灯箱
*/
export const ImageGalleryLightbox = ({ images, initialIndex = 0, ...props }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const handleOpen = (index) => {
setCurrentIndex(index);
onOpen();
};
const handlePrev = () => {
setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
};
const handleNext = () => {
setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
};
const handleKeyDown = (e) => {
if (e.key === 'ArrowLeft') handlePrev();
if (e.key === 'ArrowRight') handleNext();
if (e.key === 'Escape') onClose();
};
return (
<>
{/* 缩略图网格 */}
<HStack spacing="2" flexWrap="wrap" {...props}>
{images.map((image, index) => (
<Box
key={index}
position="relative"
cursor="pointer"
onClick={() => handleOpen(index)}
_hover={{
'& .zoom-icon': {
opacity: 1,
},
}}
>
<Image
src={image.src || image}
alt={image.alt || `图片 ${index + 1}`}
w="150px"
h="150px"
objectFit="cover"
borderRadius="md"
transition="all 0.3s"
_hover={{
transform: 'scale(1.05)',
filter: 'brightness(0.8)',
}}
/>
{/* 放大图标 */}
<Box
className="zoom-icon"
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
opacity={0}
transition="opacity 0.3s"
pointerEvents="none"
>
<Box bg="blackAlpha.700" borderRadius="full" p="2">
<ZoomIn size={24} color="white" />
</Box>
</Box>
</Box>
))}
</HStack>
{/* 灯箱模态框(带轮播) */}
<Modal
isOpen={isOpen}
onClose={onClose}
size="full"
isCentered
onKeyDown={handleKeyDown}
>
<ModalOverlay bg="blackAlpha.900" backdropFilter="blur(10px)" />
<ModalContent bg="transparent" boxShadow="none">
{/* 关闭按钮 */}
<IconButton
icon={<X />}
position="fixed"
top="4"
right="4"
size="lg"
color="white"
bg="blackAlpha.600"
_hover={{ bg: 'blackAlpha.800' }}
borderRadius="full"
zIndex={2}
onClick={onClose}
/>
<ModalBody
display="flex"
alignItems="center"
justifyContent="center"
p="0"
position="relative"
>
{/* 左箭头 */}
{images.length > 1 && (
<IconButton
icon={<ChevronLeft />}
position="absolute"
left="4"
top="50%"
transform="translateY(-50%)"
size="lg"
color="white"
bg="blackAlpha.600"
_hover={{ bg: 'blackAlpha.800' }}
borderRadius="full"
zIndex={2}
onClick={handlePrev}
/>
)}
{/* 图片 */}
<AnimatePresence mode="wait">
<MotionBox
key={currentIndex}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.3 }}
maxW="90vw"
maxH="90vh"
>
<Image
src={images[currentIndex].src || images[currentIndex]}
alt={images[currentIndex].alt || `图片 ${currentIndex + 1}`}
maxW="100%"
maxH="90vh"
objectFit="contain"
borderRadius="lg"
/>
</MotionBox>
</AnimatePresence>
{/* 右箭头 */}
{images.length > 1 && (
<IconButton
icon={<ChevronRight />}
position="absolute"
right="4"
top="50%"
transform="translateY(-50%)"
size="lg"
color="white"
bg="blackAlpha.600"
_hover={{ bg: 'blackAlpha.800' }}
borderRadius="full"
zIndex={2}
onClick={handleNext}
/>
)}
{/* 图片计数 */}
{images.length > 1 && (
<Box
position="absolute"
bottom="4"
left="50%"
transform="translateX(-50%)"
bg="blackAlpha.700"
color="white"
px="4"
py="2"
borderRadius="full"
fontSize="sm"
fontWeight="600"
>
{currentIndex + 1} / {images.length}
</Box>
)}
</ModalBody>
</ModalContent>
</Modal>
</>
);
};
export default ImageLightbox;

View File

@@ -0,0 +1,270 @@
/**
* 图片预览弹窗组件
* 支持多张图片左右切换、缩放、下载
*/
import React, { useState } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalCloseButton,
Image,
IconButton,
HStack,
Text,
Box,
} from '@chakra-ui/react';
import { ChevronLeft, ChevronRight, Download, ZoomIn, ZoomOut } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
const MotionBox = motion(Box);
const ImagePreviewModal = ({ isOpen, onClose, images = [], initialIndex = 0 }) => {
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const [scale, setScale] = useState(1);
// 切换到上一张
const handlePrevious = () => {
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
setScale(1); // 重置缩放
};
// 切换到下一张
const handleNext = () => {
setCurrentIndex((prev) => (prev + 1) % images.length);
setScale(1); // 重置缩放
};
// 放大
const handleZoomIn = () => {
setScale((prev) => Math.min(prev + 0.25, 3));
};
// 缩小
const handleZoomOut = () => {
setScale((prev) => Math.max(prev - 0.25, 0.5));
};
// 下载图片
const handleDownload = () => {
const link = document.createElement('a');
link.href = images[currentIndex];
link.download = `image-${currentIndex + 1}.jpg`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 键盘快捷键
React.useEffect(() => {
const handleKeyDown = (e) => {
if (!isOpen) return;
switch (e.key) {
case 'ArrowLeft':
handlePrevious();
break;
case 'ArrowRight':
handleNext();
break;
case 'Escape':
onClose();
break;
case '+':
case '=':
handleZoomIn();
break;
case '-':
handleZoomOut();
break;
default:
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, currentIndex]);
// 关闭时重置状态
const handleClose = () => {
setScale(1);
setCurrentIndex(initialIndex);
onClose();
};
if (!images || images.length === 0) return null;
return (
<Modal isOpen={isOpen} onClose={handleClose} size="full" isCentered>
<ModalOverlay bg="blackAlpha.900" backdropFilter="blur(10px)" />
<ModalContent bg="transparent" boxShadow="none" m="0">
<ModalCloseButton
size="lg"
color="white"
bg="blackAlpha.600"
_hover={{ bg: 'blackAlpha.800' }}
zIndex="2"
top="20px"
right="20px"
/>
<ModalBody
display="flex"
alignItems="center"
justifyContent="center"
position="relative"
p="0"
>
{/* 图片显示区域 */}
<AnimatePresence mode="wait">
<MotionBox
key={currentIndex}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.3 }}
display="flex"
alignItems="center"
justifyContent="center"
maxH="90vh"
maxW="90vw"
>
<Image
src={images[currentIndex]}
alt={`图片 ${currentIndex + 1}`}
maxH="90vh"
maxW="90vw"
objectFit="contain"
transform={`scale(${scale})`}
transition="transform 0.3s"
cursor={scale > 1 ? 'grab' : 'default'}
userSelect="none"
/>
</MotionBox>
</AnimatePresence>
{/* 左右切换按钮(仅多张图片时显示) */}
{images.length > 1 && (
<>
<IconButton
icon={<ChevronLeft size={32} />}
position="absolute"
left="20px"
top="50%"
transform="translateY(-50%)"
onClick={handlePrevious}
size="lg"
borderRadius="full"
bg="blackAlpha.600"
color="white"
_hover={{ bg: 'blackAlpha.800', transform: 'translateY(-50%) scale(1.1)' }}
_active={{ transform: 'translateY(-50%) scale(0.95)' }}
aria-label="上一张"
zIndex="2"
/>
<IconButton
icon={<ChevronRight size={32} />}
position="absolute"
right="20px"
top="50%"
transform="translateY(-50%)"
onClick={handleNext}
size="lg"
borderRadius="full"
bg="blackAlpha.600"
color="white"
_hover={{ bg: 'blackAlpha.800', transform: 'translateY(-50%) scale(1.1)' }}
_active={{ transform: 'translateY(-50%) scale(0.95)' }}
aria-label="下一张"
zIndex="2"
/>
</>
)}
{/* 底部工具栏 */}
<Box
position="absolute"
bottom="30px"
left="50%"
transform="translateX(-50%)"
bg="blackAlpha.700"
borderRadius="full"
px="6"
py="3"
backdropFilter="blur(10px)"
zIndex="2"
>
<HStack spacing="4">
{/* 缩放控制 */}
<HStack spacing="2">
<IconButton
icon={<ZoomOut size={18} />}
size="sm"
variant="ghost"
color="white"
onClick={handleZoomOut}
isDisabled={scale <= 0.5}
_hover={{ bg: 'whiteAlpha.200' }}
aria-label="缩小"
/>
<Text color="white" fontSize="sm" fontWeight="500" minW="60px" textAlign="center">
{Math.round(scale * 100)}%
</Text>
<IconButton
icon={<ZoomIn size={18} />}
size="sm"
variant="ghost"
color="white"
onClick={handleZoomIn}
isDisabled={scale >= 3}
_hover={{ bg: 'whiteAlpha.200' }}
aria-label="放大"
/>
</HStack>
{/* 下载按钮 */}
<IconButton
icon={<Download size={18} />}
size="sm"
variant="ghost"
color="white"
onClick={handleDownload}
_hover={{ bg: 'whiteAlpha.200' }}
aria-label="下载图片"
/>
{/* 图片计数(仅多张图片时显示) */}
{images.length > 1 && (
<Text color="white" fontSize="sm" fontWeight="500">
{currentIndex + 1} / {images.length}
</Text>
)}
</HStack>
</Box>
{/* 快捷键提示 */}
<Box
position="absolute"
top="80px"
left="20px"
bg="blackAlpha.600"
borderRadius="md"
px="4"
py="2"
backdropFilter="blur(10px)"
>
<Text color="whiteAlpha.800" fontSize="xs">
快捷键: 切换 | + - 缩放 | ESC 关闭
</Text>
</Box>
</ModalBody>
</ModalContent>
</Modal>
);
};
export default ImagePreviewModal;

View File

@@ -54,13 +54,11 @@ import { WatchlistMenu, FollowingEventsMenu } from './components/FeatureMenus';
import { useWatchlist } from '../../hooks/useWatchlist';
import { useFollowingEvents } from '../../hooks/useFollowingEvents';
// Phase 7 优化: 提取的二级导航、资料完整性、右侧功能区组件
import SecondaryNav from './components/SecondaryNav';
// Phase 7 优化: 提取的资料完整性、右侧功能区组件
import ProfileCompletenessAlert from './components/ProfileCompletenessAlert';
import { useProfileCompleteness } from '../../hooks/useProfileCompleteness';
import NavbarActions from './components/NavbarActions';
// Phase 7: SecondaryNav 组件已提取到 ./components/SecondaryNav/index.js
// Phase 4: MoreNavMenu 和 NavItems 组件已提取到 Navigation 目录
export default function HomeNavbar() {
@@ -152,8 +150,10 @@ export default function HomeNavbar() {
)}
<Box
position="sticky"
position="fixed"
top={showCompletenessAlert ? "60px" : 0}
left={0}
right={0}
zIndex={1000}
bg={navbarBg}
backdropFilter="blur(10px)"
@@ -167,19 +167,8 @@ export default function HomeNavbar() {
<BrandLogo />
{/* 中间导航区域 - 响应式 (Phase 4 优化) */}
{isMobile ? (
// 移动端:汉堡菜单
<IconButton
icon={<HamburgerIcon />}
variant="ghost"
onClick={onOpen}
aria-label="Open menu"
/>
) : isTablet ? (
// 中屏(平板):"更多"下拉菜单
<MoreMenu isAuthenticated={isAuthenticated} user={user} />
) : (
// 大屏(桌面):完整导航菜单
{isDesktop && (
// 桌面端:完整导航菜单(移动端和平板端的汉堡菜单已移至右侧)
<DesktopNav isAuthenticated={isAuthenticated} user={user} />
)}
@@ -189,6 +178,9 @@ export default function HomeNavbar() {
isAuthenticated={isAuthenticated}
user={user}
isDesktop={isDesktop}
isTablet={isTablet}
isMobile={isMobile}
onMenuOpen={onOpen}
handleLogout={handleLogout}
watchlistQuotes={watchlistQuotes}
followingEvents={followingEvents}
@@ -207,9 +199,6 @@ export default function HomeNavbar() {
/>
</Box>
{/* 二级导航栏 - 显示当前页面所属的二级菜单 */}
{!isMobile && <SecondaryNav showCompletenessAlert={showCompletenessAlert} />}
{/* 投资日历 Modal - 已移至 CalendarButton 组件内部 */}
</>
);

View File

@@ -2,13 +2,14 @@
// Navbar 右侧功能区组件
import React, { memo } from 'react';
import { HStack, Spinner } from '@chakra-ui/react';
import ThemeToggleButton from '../ThemeToggleButton';
import { HStack, Spinner, IconButton, Box } from '@chakra-ui/react';
import { HamburgerIcon } from '@chakra-ui/icons';
// import ThemeToggleButton from '../ThemeToggleButton'; // ❌ 已删除 - 不再支持深色模式切换
import LoginButton from '../LoginButton';
import CalendarButton from '../CalendarButton';
import { WatchlistMenu, FollowingEventsMenu } from '../FeatureMenus';
import { DesktopUserMenu, TabletUserMenu } from '../UserMenu';
import { PersonalCenterMenu } from '../Navigation';
import { PersonalCenterMenu, MoreMenu } from '../Navigation';
/**
* Navbar 右侧功能区组件
@@ -19,6 +20,9 @@ import { PersonalCenterMenu } from '../Navigation';
* @param {boolean} props.isAuthenticated - 是否已登录
* @param {Object} props.user - 用户对象
* @param {boolean} props.isDesktop - 是否为桌面端
* @param {boolean} props.isTablet - 是否为平板端
* @param {boolean} props.isMobile - 是否为移动端
* @param {Function} props.onMenuOpen - 打开移动端抽屉菜单的回调
* @param {Function} props.handleLogout - 登出回调
* @param {Array} props.watchlistQuotes - 自选股数据(用于 TabletUserMenu
* @param {Array} props.followingEvents - 关注事件数据(用于 TabletUserMenu
@@ -28,15 +32,15 @@ const NavbarActions = memo(({
isAuthenticated,
user,
isDesktop,
isTablet,
isMobile,
onMenuOpen,
handleLogout,
watchlistQuotes,
followingEvents
}) => {
return (
<HStack spacing={{ base: 2, md: 4 }}>
{/* 主题切换按钮 */}
<ThemeToggleButton />
{/* 显示加载状态 */}
{isLoading ? (
<Spinner size="sm" color="blue.500" />
@@ -64,13 +68,26 @@ const NavbarActions = memo(({
/>
)}
{/* 个人中心下拉菜单 - 仅大屏显示 */}
{isDesktop && (
{/* 头像右侧的菜单 - 响应式(互斥逻辑,确保只渲染一个) */}
{isDesktop ? (
// 桌面端:个人中心下拉菜单
<PersonalCenterMenu user={user} handleLogout={handleLogout} />
) : isTablet ? (
// 平板端MoreMenu 下拉菜单
<MoreMenu isAuthenticated={isAuthenticated} user={user} />
) : (
// 移动端:汉堡菜单(打开抽屉)
<IconButton
icon={<HamburgerIcon />}
variant="ghost"
onClick={onMenuOpen}
aria-label="打开菜单"
size="md"
/>
)}
</HStack>
) : (
// 未登录状态 - 单一按钮
// 未登录状态 - 仅显示登录按钮
<LoginButton />
)}
</HStack>

View File

@@ -1,111 +0,0 @@
// src/components/Navbars/components/SecondaryNav/config.js
// 二级导航配置数据
/**
* 二级导航配置结构
* - key: 匹配的路径前缀
* - title: 导航组标题
* - items: 导航项列表
* - path: 路径
* - label: 显示文本
* - badges: 徽章列表 (可选)
* - external: 是否外部链接 (可选)
*/
export const secondaryNavConfig = {
'/community': {
title: '高频跟踪',
items: [
{
path: '/community',
label: '事件中心',
badges: [
{ text: 'HOT', colorScheme: 'green' },
{ text: 'NEW', colorScheme: 'red' }
]
},
{
path: '/concepts',
label: '概念中心',
badges: [{ text: 'NEW', colorScheme: 'red' }]
}
]
},
'/concepts': {
title: '高频跟踪',
items: [
{
path: '/community',
label: '事件中心',
badges: [
{ text: 'HOT', colorScheme: 'green' },
{ text: 'NEW', colorScheme: 'red' }
]
},
{
path: '/concepts',
label: '概念中心',
badges: [{ text: 'NEW', colorScheme: 'red' }]
}
]
},
'/limit-analyse': {
title: '行情复盘',
items: [
{
path: '/limit-analyse',
label: '涨停分析',
badges: [{ text: 'FREE', colorScheme: 'blue' }]
},
{
path: '/stocks',
label: '个股中心',
badges: [{ text: 'HOT', colorScheme: 'green' }]
},
{
path: '/trading-simulation',
label: '模拟盘',
badges: [{ text: 'NEW', colorScheme: 'red' }]
}
]
},
'/stocks': {
title: '行情复盘',
items: [
{
path: '/limit-analyse',
label: '涨停分析',
badges: [{ text: 'FREE', colorScheme: 'blue' }]
},
{
path: '/stocks',
label: '个股中心',
badges: [{ text: 'HOT', colorScheme: 'green' }]
},
{
path: '/trading-simulation',
label: '模拟盘',
badges: [{ text: 'NEW', colorScheme: 'red' }]
}
]
},
'/trading-simulation': {
title: '行情复盘',
items: [
{
path: '/limit-analyse',
label: '涨停分析',
badges: [{ text: 'FREE', colorScheme: 'blue' }]
},
{
path: '/stocks',
label: '个股中心',
badges: [{ text: 'HOT', colorScheme: 'green' }]
},
{
path: '/trading-simulation',
label: '模拟盘',
badges: [{ text: 'NEW', colorScheme: 'red' }]
}
]
}
};

View File

@@ -1,138 +0,0 @@
// src/components/Navbars/components/SecondaryNav/index.js
// 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项
import React, { memo } from 'react';
import {
Box,
Container,
HStack,
Text,
Button,
Flex,
Badge,
useColorModeValue
} from '@chakra-ui/react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useNavigationEvents } from '../../../../hooks/useNavigationEvents';
import { secondaryNavConfig } from './config';
/**
* 二级导航栏组件
* 根据当前路径显示对应的二级菜单项
*
* @param {Object} props
* @param {boolean} props.showCompletenessAlert - 是否显示完整性提醒(影响 sticky top 位置)
*/
const SecondaryNav = memo(({ showCompletenessAlert }) => {
const navigate = useNavigate();
const location = useLocation();
// 颜色模式
const navbarBg = useColorModeValue('gray.50', 'gray.700');
const itemHoverBg = useColorModeValue('white', 'gray.600');
const borderColorValue = useColorModeValue('gray.200', 'gray.600');
// 导航埋点
const navEvents = useNavigationEvents({ component: 'secondary_nav' });
// 找到当前路径对应的二级导航配置
const currentConfig = Object.keys(secondaryNavConfig).find(key =>
location.pathname.includes(key)
);
// 如果没有匹配的二级导航,不显示
if (!currentConfig) return null;
const config = secondaryNavConfig[currentConfig];
return (
<Box
bg={navbarBg}
borderBottom="1px"
borderColor={borderColorValue}
py={2}
position="sticky"
top={showCompletenessAlert ? "120px" : "60px"}
zIndex={100}
>
<Container maxW="container.xl" px={4}>
<HStack spacing={1}>
{/* 显示一级菜单标题 */}
<Text fontSize="sm" color="gray.500" mr={2}>
{config.title}:
</Text>
{/* 二级菜单项 */}
{config.items.map((item, index) => {
const isActive = location.pathname.includes(item.path);
return item.external ? (
<Button
key={index}
as="a"
href={item.path}
size="sm"
variant="ghost"
bg="transparent"
color="inherit"
fontWeight="normal"
_hover={{ bg: itemHoverBg }}
borderRadius="md"
px={3}
>
<Flex align="center" gap={2}>
<Text>{item.label}</Text>
{item.badges && item.badges.length > 0 && (
<HStack spacing={1}>
{item.badges.map((badge, bIndex) => (
<Badge key={bIndex} size="xs" colorScheme={badge.colorScheme}>
{badge.text}
</Badge>
))}
</HStack>
)}
</Flex>
</Button>
) : (
<Button
key={index}
onClick={() => {
// 追踪侧边栏菜单点击
navEvents.trackSidebarMenuClicked(item.label, item.path, 2, false);
navigate(item.path);
}}
size="sm"
variant="ghost"
bg={isActive ? 'blue.50' : 'transparent'}
color={isActive ? 'blue.600' : 'inherit'}
fontWeight={isActive ? 'bold' : 'normal'}
borderBottom={isActive ? '2px solid' : 'none'}
borderColor="blue.600"
borderRadius={isActive ? '0' : 'md'}
_hover={{ bg: isActive ? 'blue.100' : itemHoverBg }}
px={3}
>
<Flex align="center" gap={2}>
<Text>{item.label}</Text>
{item.badges && item.badges.length > 0 && (
<HStack spacing={1}>
{item.badges.map((badge, bIndex) => (
<Badge key={bIndex} size="xs" colorScheme={badge.colorScheme}>
{badge.text}
</Badge>
))}
</HStack>
)}
</Flex>
</Button>
);
})}
</HStack>
</Container>
</Box>
);
});
SecondaryNav.displayName = 'SecondaryNav';
export default SecondaryNav;

View File

@@ -1,51 +0,0 @@
// src/components/Navbars/components/ThemeToggleButton.js
// 主题切换按钮组件 - Phase 7 优化:添加导航埋点支持
import React, { memo } from 'react';
import { IconButton, useColorMode } from '@chakra-ui/react';
import { SunIcon, MoonIcon } from '@chakra-ui/icons';
import { useNavigationEvents } from '../../../hooks/useNavigationEvents';
/**
* 主题切换按钮组件
* 支持在亮色和暗色主题之间切换,包含导航埋点
*
* 性能优化:
* - 使用 memo 避免父组件重新渲染时的不必要更新
* - 只依赖 colorMode当主题切换时才重新渲染
*
* @param {Object} props
* @param {string} props.size - 按钮大小,默认 'sm'
* @param {string} props.variant - 按钮样式,默认 'ghost'
* @returns {JSX.Element}
*/
const ThemeToggleButton = memo(({ size = 'sm', variant = 'ghost' }) => {
const { colorMode, toggleColorMode } = useColorMode();
const navEvents = useNavigationEvents({ component: 'theme_toggle' });
const handleToggle = () => {
// 追踪主题切换
const fromTheme = colorMode;
const toTheme = colorMode === 'light' ? 'dark' : 'light';
navEvents.trackThemeChanged(fromTheme, toTheme);
// 切换主题
toggleColorMode();
};
return (
<IconButton
aria-label="切换主题"
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
onClick={handleToggle}
variant={variant}
size={size}
minW={{ base: '36px', md: '40px' }}
minH={{ base: '36px', md: '40px' }}
/>
);
});
ThemeToggleButton.displayName = 'ThemeToggleButton';
export default ThemeToggleButton;

View File

@@ -1,138 +0,0 @@
import { useRef, useState } from "react";
import { Splide, SplideTrack, SplideSlide } from "@splidejs/react-splide";
import Button from "@/components/Button";
import Image from "@/components/Image";
import { pricing } from "@/mocks/pricing";
type PricingListProps = {
monthly?: boolean;
};
const PricingList = ({ monthly = true }: PricingListProps) => {
const [activeIndex, setActiveIndex] = useState<number>(0);
const ref = useRef<any>(null);
const handleClick = (index: number) => {
setActiveIndex(index);
ref.current?.go(index);
};
return (
<Splide
className="splide-pricing splide-visible"
options={{
mediaQuery: "min",
autoWidth: true,
pagination: false,
arrows: false,
gap: "1rem",
breakpoints: {
1024: {
destroy: true,
},
},
}}
onMoved={(e, newIndex) => setActiveIndex(newIndex)}
hasTrack={false}
ref={ref}
>
<SplideTrack>
{pricing.map((item, index) => (
<SplideSlide
className={`${index === 1 ? "" : "py-3"}`}
key={item.id}
>
<div
className={`w-[19rem] h-full px-6 ${
index === 1 ? "py-12" : "py-8"
} bg-n-8 border border-n-6 rounded-[2rem] lg:w-auto`}
key={item.id}
>
<h4
className={`h4 mb-4 ${
index === 0 ? "text-color-2" : ""
} ${index === 1 ? "text-color-1" : ""} ${
index === 2 ? "text-color-3" : ""
}`}
>
{item.title}
</h4>
<p className="body-2 min-h-[4rem] mb-3 text-n-1/50">
{item.description}
</p>
<div className="flex items-center h-[5.5rem] mb-6">
{item.price && (
<>
<div className="h3">$</div>
<div className="text-[5.5rem] leading-none font-bold">
{monthly
? item.price
: item.price !== "0"
? (
+item.price *
12 *
0.9
).toFixed(1)
: item.price}
</div>
</>
)}
</div>
<Button
className="w-full mb-6"
href={
item.price
? "/pricing"
: "mailto:info@ui8.net"
}
white={!!item.price}
>
{item.price ? "Get started" : "Contact us"}
</Button>
<ul>
{item.features.map((feature, index) => (
<li
className="flex items-start py-5 border-t border-n-6"
key={index}
>
<Image
src="/images/check.svg"
width={24}
height={24}
alt="Check"
/>
<p className="body-2 ml-4">{feature}</p>
</li>
))}
</ul>
</div>
</SplideSlide>
))}
</SplideTrack>
<div className="flex justify-center mt-8 -mx-2 md:mt-15 lg:hidden">
{pricing.map((item, index) => (
<button
className="relative w-6 h-6 mx-2"
onClick={() => handleClick(index)}
key={item.id}
>
<span
className={`absolute inset-0 bg-conic-gradient rounded-full transition-opacity ${
index === activeIndex
? "opacity-100"
: "opacity-0"
}`}
></span>
<span className="absolute inset-0.25 bg-n-8 rounded-full">
<span className="absolute inset-2 bg-n-1 rounded-full"></span>
</span>
</button>
))}
</div>
</Splide>
);
};
export default PricingList;

View File

@@ -1,138 +0,0 @@
import { useRef, useState } from "react";
import { Splide, SplideTrack, SplideSlide } from "@splidejs/react-splide";
import Button from "@/components/Button";
import Image from "@/components/Image";
import { pricing } from "@/mocks/pricing";
type PricingListProps = {
monthly?: boolean;
};
const PricingList = ({ monthly = true }: PricingListProps) => {
const [activeIndex, setActiveIndex] = useState<number>(0);
const ref = useRef<any>(null);
const handleClick = (index: number) => {
setActiveIndex(index);
ref.current?.go(index);
};
return (
<Splide
className="splide-pricing splide-visible"
options={{
mediaQuery: "min",
autoWidth: true,
pagination: false,
arrows: false,
gap: "1rem",
breakpoints: {
1024: {
destroy: true,
},
},
}}
onMoved={(e, newIndex) => setActiveIndex(newIndex)}
hasTrack={false}
ref={ref}
>
<SplideTrack>
{pricing.map((item, index) => (
<SplideSlide
className={`${index === 1 ? "" : "py-3"}`}
key={item.id}
>
<div
className={`w-[19rem] h-full px-6 ${
index === 1 ? "py-12" : "py-8"
} bg-n-8 border border-n-6 rounded-[2rem] lg:w-auto`}
key={item.id}
>
<h4
className={`h4 mb-4 ${
index === 0 ? "text-color-2" : ""
} ${index === 1 ? "text-color-1" : ""} ${
index === 2 ? "text-color-3" : ""
}`}
>
{item.title}
</h4>
<p className="body-2 min-h-[4rem] mb-3 text-n-1/50">
{item.description}
</p>
<div className="flex items-center h-[5.5rem] mb-6">
{item.price && (
<>
<div className="h3">$</div>
<div className="text-[5.5rem] leading-none font-bold">
{monthly
? item.price
: item.price !== "0"
? (
+item.price *
12 *
0.9
).toFixed(1)
: item.price}
</div>
</>
)}
</div>
<Button
className="w-full mb-6"
href={
item.price
? "/pricing"
: "mailto:info@ui8.net"
}
white={!!item.price}
>
{item.price ? "Get started" : "Contact us"}
</Button>
<ul>
{item.features.map((feature, index) => (
<li
className="flex items-start py-5 border-t border-n-6"
key={index}
>
<Image
src="/images/check.svg"
width={24}
height={24}
alt="Check"
/>
<p className="body-2 ml-4">{feature}</p>
</li>
))}
</ul>
</div>
</SplideSlide>
))}
</SplideTrack>
<div className="flex justify-center mt-8 -mx-2 md:mt-15 lg:hidden">
{pricing.map((item, index) => (
<button
className="relative w-6 h-6 mx-2"
onClick={() => handleClick(index)}
key={item.id}
>
<span
className={`absolute inset-0 bg-conic-gradient rounded-full transition-opacity ${
index === activeIndex
? "opacity-100"
: "opacity-0"
}`}
></span>
<span className="absolute inset-0.25 bg-n-8 rounded-full">
<span className="absolute inset-2 bg-n-1 rounded-full"></span>
</span>
</button>
))}
</div>
</Splide>
);
};
export default PricingList;

View File

@@ -0,0 +1,614 @@
// src/components/StockChart/KLineChartModal.tsx - K线图弹窗组件
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import * as echarts from 'echarts';
import { stockService } from '@services/eventService';
import { logger } from '@utils/logger';
/**
* 股票信息
*/
interface StockInfo {
stock_code: string;
stock_name?: string;
}
/**
* KLineChartModal 组件 Props
*/
export interface KLineChartModalProps {
/** 模态框是否打开 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 股票信息 */
stock: StockInfo | null;
/** 事件时间 */
eventTime?: string | null;
/** 模态框大小 */
size?: string;
}
/**
* K线数据点
*/
interface KLineDataPoint {
time: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
}
const KLineChartModal: React.FC<KLineChartModalProps> = ({
isOpen,
onClose,
stock,
eventTime,
size = '5xl',
}) => {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<KLineDataPoint[]>([]);
// 调试日志
console.log('[KLineChartModal] 渲染状态:', {
isOpen,
stock,
eventTime,
dataLength: data.length,
loading,
error
});
// 加载K线数据
const loadData = async () => {
if (!stock?.stock_code) return;
setLoading(true);
setError(null);
try {
logger.debug('KLineChartModal', '开始加载K线数据 (loadData)', {
stockCode: stock.stock_code,
eventTime,
});
const response = await stockService.getKlineData(
stock.stock_code,
'daily',
eventTime || undefined
);
console.log('[KLineChartModal] API响应:', response);
if (!response || !response.data || response.data.length === 0) {
throw new Error('暂无K线数据');
}
console.log('[KLineChartModal] 数据条数:', response.data.length);
setData(response.data);
logger.info('KLineChartModal', 'K线数据加载成功 (loadData)', {
dataCount: response.data.length,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : '数据加载失败';
logger.error('KLineChartModal', 'loadData', err as Error);
setError(errorMsg);
} finally {
setLoading(false);
}
};
// 初始化图表
useEffect(() => {
if (!isOpen) return;
// 延迟初始化,确保 Modal 动画完成后 DOM 已经渲染
const timer = setTimeout(() => {
if (!chartRef.current) {
console.error('[KLineChartModal] DOM元素未找到无法初始化图表');
return;
}
console.log('[KLineChartModal] 初始化图表...');
// 创建图表实例不使用主题直接在option中配置背景色
chartInstance.current = echarts.init(chartRef.current);
console.log('[KLineChartModal] 图表实例创建成功');
// 监听窗口大小变化
const handleResize = () => {
chartInstance.current?.resize();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, 100); // 延迟100ms等待Modal完全打开
return () => {
clearTimeout(timer);
if (chartInstance.current) {
chartInstance.current.dispose();
chartInstance.current = null;
}
};
}, [isOpen]);
// 更新图表数据
useEffect(() => {
if (data.length === 0) {
console.log('[KLineChartModal] 无数据,跳过图表更新');
return;
}
const updateChart = () => {
if (!chartInstance.current) {
console.warn('[KLineChartModal] 图表实例不存在');
return false;
}
console.log('[KLineChartModal] 开始更新图表,数据点:', data.length);
const dates = data.map((d) => d.time);
const klineData = data.map((d) => [d.open, d.close, d.low, d.high]);
const volumes = data.map((d) => d.volume);
// 计算成交量柱子颜色(涨为红,跌为绿)
const volumeColors = data.map((d) =>
d.close >= d.open ? '#ef5350' : '#26a69a'
);
// 提取事件发生日期YYYY-MM-DD格式
let eventDateStr: string | null = null;
if (eventTime) {
try {
const eventDate = new Date(eventTime);
const year = eventDate.getFullYear();
const month = (eventDate.getMonth() + 1).toString().padStart(2, '0');
const day = eventDate.getDate().toString().padStart(2, '0');
eventDateStr = `${year}-${month}-${day}`;
console.log('[KLineChartModal] 事件发生日期:', eventDateStr);
} catch (e) {
console.error('[KLineChartModal] 解析事件日期失败:', e);
}
}
// 图表配置
const option: echarts.EChartsOption = {
backgroundColor: '#1a1a1a',
title: {
text: `${stock?.stock_name || stock?.stock_code} - 日K线`,
left: 'center',
top: 10,
textStyle: {
color: '#e0e0e0',
fontSize: 18,
fontWeight: 'bold',
},
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(30, 30, 30, 0.95)',
borderColor: '#404040',
borderWidth: 1,
textStyle: {
color: '#e0e0e0',
},
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999',
},
},
formatter: (params: any) => {
const dataIndex = params[0]?.dataIndex;
if (dataIndex === undefined) return '';
const item = data[dataIndex];
const change = item.close - item.open;
const changePercent = (change / item.open) * 100;
const changeColor = change >= 0 ? '#ef5350' : '#26a69a';
const changeSign = change >= 0 ? '+' : '';
return `
<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 8px;">${item.time}</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>开盘:</span>
<span style="margin-left: 20px;">${item.open.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>收盘:</span>
<span style="color: ${changeColor}; font-weight: bold; margin-left: 20px;">${item.close.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>最高:</span>
<span style="margin-left: 20px;">${item.high.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>最低:</span>
<span style="margin-left: 20px;">${item.low.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>涨跌额:</span>
<span style="color: ${changeColor}; margin-left: 20px;">${changeSign}${change.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>涨跌幅:</span>
<span style="color: ${changeColor}; margin-left: 20px;">${changeSign}${changePercent.toFixed(2)}%</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>成交量:</span>
<span style="margin-left: 20px;">${(item.volume / 100).toFixed(0)}手</span>
</div>
</div>
`;
},
},
grid: [
{
left: '5%',
right: '5%',
top: '12%',
height: '60%',
},
{
left: '5%',
right: '5%',
top: '77%',
height: '18%',
},
],
xAxis: [
{
type: 'category',
data: dates,
gridIndex: 0,
axisLine: {
lineStyle: {
color: '#404040',
},
},
axisLabel: {
color: '#999',
interval: Math.floor(dates.length / 8),
},
splitLine: {
show: false,
},
},
{
type: 'category',
data: dates,
gridIndex: 1,
axisLine: {
lineStyle: {
color: '#404040',
},
},
axisLabel: {
color: '#999',
interval: Math.floor(dates.length / 8),
},
},
],
yAxis: [
{
scale: true,
gridIndex: 0,
splitLine: {
show: true,
lineStyle: {
color: '#2a2a2a',
},
},
axisLine: {
lineStyle: {
color: '#404040',
},
},
axisLabel: {
color: '#999',
formatter: (value: number) => value.toFixed(2),
},
},
{
scale: true,
gridIndex: 1,
splitLine: {
show: false,
},
axisLine: {
lineStyle: {
color: '#404040',
},
},
axisLabel: {
color: '#999',
formatter: (value: number) => {
if (value >= 100000000) {
return (value / 100000000).toFixed(1) + '亿';
} else if (value >= 10000) {
return (value / 10000).toFixed(1) + '万';
}
return value.toFixed(0);
},
},
},
],
series: [
{
name: 'K线',
type: 'candlestick',
data: klineData,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: '#ef5350', // 涨
color0: '#26a69a', // 跌
borderColor: '#ef5350',
borderColor0: '#26a69a',
},
markLine: eventDateStr ? {
silent: false,
symbol: 'none',
label: {
show: true,
position: 'insideEndTop',
formatter: '事件发生',
color: '#ffd700',
fontSize: 12,
fontWeight: 'bold',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
padding: [4, 8],
borderRadius: 4,
},
lineStyle: {
color: '#ffd700',
width: 2,
type: 'solid',
},
data: [
{
xAxis: eventDateStr,
label: {
formatter: '⚡ 事件发生',
},
},
],
} : undefined,
},
{
name: '成交量',
type: 'bar',
data: volumes,
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: (params: any) => {
return volumeColors[params.dataIndex];
},
},
},
],
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1],
start: 0,
end: 100,
},
],
};
chartInstance.current.setOption(option);
console.log('[KLineChartModal] 图表option已设置');
// 强制resize以确保图表正确显示
setTimeout(() => {
chartInstance.current?.resize();
console.log('[KLineChartModal] 图表已resize');
}, 100);
return true;
};
// 立即尝试更新,如果失败则重试
if (!updateChart()) {
console.log('[KLineChartModal] 第一次更新失败200ms后重试...');
const retryTimer = setTimeout(() => {
updateChart();
}, 200);
return () => clearTimeout(retryTimer);
}
}, [data, stock]);
// 加载数据
useEffect(() => {
if (isOpen) {
loadData();
}
}, [isOpen, stock?.stock_code, eventTime]);
// 创建或获取 Portal 容器
useEffect(() => {
let container = document.getElementById('kline-modal-root');
if (!container) {
container = document.createElement('div');
container.id = 'kline-modal-root';
container.style.cssText = 'position: fixed; top: 0; left: 0; z-index: 10000;';
document.body.appendChild(container);
}
return () => {
// 组件卸载时不删除容器,因为可能会被复用
};
}, []);
if (!stock) return null;
console.log('[KLineChartModal] 渲染 Modal, isOpen:', isOpen);
// 获取 Portal 容器
const portalContainer = document.getElementById('kline-modal-root') || document.body;
// 如果不显示则返回 null
if (!isOpen) return null;
const modalContent = (
<>
{/* 遮罩层 */}
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
zIndex: 10001,
}}
onClick={onClose}
/>
{/* 弹窗内容 */}
<div
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '90vw',
maxWidth: '1400px',
maxHeight: '85vh',
backgroundColor: '#1a1a1a',
border: '2px solid #ffd700',
boxShadow: '0 0 30px rgba(255, 215, 0, 0.5)',
borderRadius: '8px',
zIndex: 10002,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
{/* Header */}
<div
style={{
padding: '16px 24px',
borderBottom: '1px solid #404040',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
}}
>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#e0e0e0' }}>
{stock.stock_name || stock.stock_code} ({stock.stock_code})
</span>
{data.length > 0 && (
<span style={{ fontSize: '12px', color: '#666', fontStyle: 'italic' }}>
{data.length}1
</span>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginTop: '4px' }}>
<span style={{ fontSize: '14px', color: '#999' }}>K线图</span>
<span style={{ fontSize: '12px', color: '#666' }}>
💡 |
</span>
</div>
</div>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: '#999',
fontSize: '24px',
cursor: 'pointer',
padding: '0 8px',
lineHeight: '1',
}}
onMouseOver={(e) => (e.currentTarget.style.color = '#e0e0e0')}
onMouseOut={(e) => (e.currentTarget.style.color = '#999')}
>
×
</button>
</div>
{/* Body */}
<div style={{ padding: '16px', flex: 1, overflow: 'auto' }}>
{error && (
<div
style={{
backgroundColor: '#2a1a1a',
border: '1px solid #ef5350',
borderRadius: '4px',
padding: '12px 16px',
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<span style={{ color: '#ef5350' }}></span>
<span style={{ color: '#e0e0e0' }}>{error}</span>
</div>
)}
<div style={{ position: 'relative', height: '680px', width: '100%' }}>
{loading && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(26, 26, 26, 0.7)',
zIndex: 10,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '16px',
}}
>
<div
style={{
width: '40px',
height: '40px',
border: '3px solid #404040',
borderTop: '3px solid #3182ce',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
}}
/>
<span style={{ color: '#e0e0e0' }}>K线数据...</span>
</div>
)}
<div ref={chartRef} style={{ width: '100%', height: '100%' }} />
</div>
</div>
</div>
{/* 添加旋转动画的 CSS */}
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</>
);
return createPortal(modalContent, portalContainer);
};
export default KLineChartModal;

View File

@@ -507,8 +507,9 @@ const StockChartAntdModal = ({
footer={null}
onCancel={onCancel}
width={width}
style={{ position: fixed ? 'fixed' : 'absolute', left: fixed ? 50 : 0, top: fixed ? 50 : 80, zIndex: 2000 }}
mask={false}
centered
zIndex={2500}
mask={true}
destroyOnClose={true}
bodyStyle={{ maxHeight: 'calc(90vh - 120px)', overflowY: 'auto', padding: '16px' }}
>

View File

@@ -0,0 +1,287 @@
/**
* StockChartKLineModal - K 线图表模态框组件
*
* 使用 KLineChart 库实现的专业金融图表组件
* 替换原有的 ECharts 实现StockChartAntdModal.js
*/
import React, { useState, useCallback, useMemo } from 'react';
import { Modal, Button, Radio, Select, Space, Spin, Alert } from 'antd';
import type { RadioChangeEvent } from 'antd';
import {
LineChartOutlined,
BarChartOutlined,
SettingOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import { Box } from '@chakra-ui/react';
// 自定义 Hooks
import { useKLineChart, useKLineData, useEventMarker } from './hooks';
// 类型定义
import type { ChartType, StockInfo } from './types';
// 配置常量
import {
CHART_TYPE_CONFIG,
CHART_HEIGHTS,
INDICATORS,
DEFAULT_SUB_INDICATORS,
} from './config';
// 工具函数
import { createSubIndicators } from './utils';
// 日志
import { logger } from '@utils/logger';
// ==================== 组件 Props ====================
export interface StockChartKLineModalProps {
/** 是否显示模态框 */
visible: boolean;
/** 关闭模态框回调 */
onClose: () => void;
/** 股票信息 */
stock: StockInfo;
/** 事件时间ISO 字符串,可选) */
eventTime?: string;
/** 事件标题(用于标记标签,可选) */
eventTitle?: string;
}
// ==================== 主组件 ====================
const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
visible,
onClose,
stock,
eventTime,
eventTitle,
}) => {
// ==================== 状态管理 ====================
/** 图表类型(分时图/日K线 */
const [chartType, setChartType] = useState<ChartType>('daily');
/** 选中的副图指标 */
const [selectedIndicators, setSelectedIndicators] = useState<string[]>(
DEFAULT_SUB_INDICATORS
);
// ==================== 自定义 Hooks ====================
/** 图表实例管理 */
const { chart, chartRef, isInitialized, error: chartError } = useKLineChart({
containerId: `kline-chart-${stock.stock_code}`,
height: CHART_HEIGHTS.main,
autoResize: true,
});
/** 数据加载管理 */
const {
data,
loading: dataLoading,
error: dataError,
loadData,
} = useKLineData({
chart,
stockCode: stock.stock_code,
chartType,
eventTime,
autoLoad: visible, // 模态框打开时自动加载
});
/** 事件标记管理 */
const { marker } = useEventMarker({
chart,
data,
eventTime,
eventTitle,
autoCreate: true,
});
// ==================== 事件处理 ====================
/**
* 切换图表类型(分时图 ↔ 日K线
*/
const handleChartTypeChange = useCallback((e: RadioChangeEvent) => {
const newType = e.target.value as ChartType;
setChartType(newType);
logger.debug('StockChartKLineModal', '切换图表类型 (handleChartTypeChange)', {
newType,
});
}, []);
/**
* 切换副图指标
*/
const handleIndicatorChange = useCallback(
(values: string[]) => {
setSelectedIndicators(values);
if (!chart) {
return;
}
// 先移除所有副图指标KLineChart 会自动移除)
// 然后创建新的指标
createSubIndicators(chart, values);
logger.debug('StockChartKLineModal', '切换副图指标 (handleIndicatorChange)', {
indicators: values,
});
},
[chart]
);
/**
* 刷新数据
*/
const handleRefresh = useCallback(() => {
loadData();
logger.debug('StockChartKLineModal', 'handleRefresh', '刷新数据');
}, [loadData]);
// ==================== 计算属性 ====================
/** 是否有错误 */
const hasError = useMemo(() => {
return !!chartError || !!dataError;
}, [chartError, dataError]);
/** 错误消息 */
const errorMessage = useMemo(() => {
if (chartError) {
return `图表初始化失败: ${chartError.message}`;
}
if (dataError) {
return `数据加载失败: ${dataError.message}`;
}
return null;
}, [chartError, dataError]);
/** 模态框标题 */
const modalTitle = useMemo(() => {
return `${stock.stock_name}${stock.stock_code} - ${CHART_TYPE_CONFIG[chartType].label}`;
}, [stock, chartType]);
/** 是否显示加载状态 */
const showLoading = useMemo(() => {
return dataLoading || !isInitialized;
}, [dataLoading, isInitialized]);
// ==================== 副作用 ====================
// 无副作用,都在 Hooks 中管理
// ==================== 渲染 ====================
return (
<Modal
title={modalTitle}
open={visible}
onCancel={onClose}
width={1200}
footer={null}
centered
destroyOnClose // 关闭时销毁组件(释放图表资源)
>
{/* 工具栏 */}
<Box mb={4}>
<Space wrap>
{/* 图表类型切换 */}
<Radio.Group value={chartType} onChange={handleChartTypeChange}>
<Radio.Button value="timeline">
<LineChartOutlined />
</Radio.Button>
<Radio.Button value="daily">
<BarChartOutlined /> K线
</Radio.Button>
</Radio.Group>
{/* 副图指标选择 */}
<Select
mode="multiple"
placeholder="选择副图指标"
value={selectedIndicators}
onChange={handleIndicatorChange}
style={{ minWidth: 200 }}
maxTagCount={2}
>
{INDICATORS.sub.map((indicator) => (
<Select.Option key={indicator.name} value={indicator.name}>
<SettingOutlined /> {indicator.label}
</Select.Option>
))}
</Select>
{/* 刷新按钮 */}
<Button
icon={<ReloadOutlined />}
onClick={handleRefresh}
loading={dataLoading}
>
</Button>
</Space>
</Box>
{/* 错误提示 */}
{hasError && (
<Alert
message="加载失败"
description={errorMessage}
type="error"
closable
showIcon
style={{ marginBottom: 16 }}
/>
)}
{/* 图表容器 */}
<Box position="relative">
{/* 加载遮罩 */}
{showLoading && (
<Box
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
zIndex={10}
>
<Spin size="large" tip="加载中..." />
</Box>
)}
{/* KLineChart 容器 */}
<div
ref={chartRef}
id={`kline-chart-${stock.stock_code}`}
style={{
width: '100%',
height: `${CHART_HEIGHTS.main}px`,
opacity: showLoading ? 0.5 : 1,
transition: 'opacity 0.3s',
}}
/>
</Box>
{/* 数据信息(调试用,生产环境可移除) */}
{process.env.NODE_ENV === 'development' && (
<Box mt={2} fontSize="12px" color="gray.500">
<Space split="|">
<span>: {data.length}</span>
<span>: {marker ? marker.label : '无'}</span>
<span>ID: {chart?.id || '未初始化'}</span>
</Space>
</Box>
)}
</Modal>
);
};
export default StockChartKLineModal;

View File

@@ -1,5 +1,5 @@
// src/components/StockChart/StockChartModal.js - 统一的股票图表组件
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, Button, ButtonGroup, VStack, HStack, Text, Badge, Box, Flex, CircularProgress } from '@chakra-ui/react';
import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts';
@@ -7,6 +7,7 @@ import dayjs from 'dayjs';
import { stockService } from '../../services/eventService';
import { logger } from '../../utils/logger';
import RiskDisclaimer from '../RiskDisclaimer';
import { RelationDescription } from '../StockRelation';
const StockChartModal = ({
isOpen,
@@ -14,34 +15,16 @@ const StockChartModal = ({
stock,
eventTime,
isChakraUI = true, // 是否使用Chakra UI默认true如果false则使用Antd
size = "6xl"
size = "6xl",
initialChartType = 'timeline' // 初始图表类型timeline/daily
}) => {
const chartRef = useRef(null);
const chartInstanceRef = useRef(null);
const [chartType, setChartType] = useState('timeline');
const [chartType, setChartType] = useState(initialChartType);
const [loading, setLoading] = useState(false);
const [chartData, setChartData] = useState(null);
const [preloadedData, setPreloadedData] = useState({});
// 处理关联描述(兼容对象和字符串格式)
const getRelationDesc = () => {
const relationDesc = stock?.relation_desc;
if (!relationDesc) return null;
if (typeof relationDesc === 'string') {
return relationDesc;
} else if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) {
// 新格式:{data: [{query_part: "...", sentences: "..."}]}
return relationDesc.data
.map(item => item.query_part || item.sentences || '')
.filter(s => s)
.join('') || null;
}
return null;
};
// 预加载数据
const preloadData = async (type) => {
if (!stock || preloadedData[type]) return;
@@ -539,7 +522,8 @@ const StockChartModal = ({
</ModalHeader>
<ModalCloseButton />
<ModalBody p={0} overflowY="auto" maxH="calc(90vh - 120px)">
<Box h="400px" w="100%" position="relative">
{/* 图表区域 */}
<Box h="500px" w="100%" position="relative">
{loading && (
<Flex
position="absolute"
@@ -558,27 +542,13 @@ const StockChartModal = ({
<div ref={chartRef} style={{ height: '100%', width: '100%', minHeight: '500px' }} />
</Box>
{getRelationDesc() && (
<Box p={4} borderTop="1px solid" borderTopColor="gray.200">
<Text fontSize="sm" fontWeight="bold" mb={2}>关联描述:</Text>
<Text fontSize="sm" color="gray.600">{getRelationDesc()}</Text>
</Box>
)}
{/* 关联描述 */}
<RelationDescription relationDesc={stock?.relation_desc} />
{/* 风险提示 */}
<Box px={4} pb={4}>
<RiskDisclaimer variant="default" />
</Box>
{process.env.NODE_ENV === 'development' && chartData && (
<Box p={4} bg="gray.50" fontSize="xs" color="gray.600">
<Text fontWeight="bold">调试信息:</Text>
<Text>数据条数: {chartData.data ? chartData.data.length : 0}</Text>
<Text>交易日期: {chartData.trade_date}</Text>
<Text>图表类型: {chartType}</Text>
<Text>原始事件时间: {eventTime}</Text>
</Box>
)}
</ModalBody>
</ModalContent>
</Modal>

View File

@@ -0,0 +1,207 @@
// src/components/StockChart/StockChartModal.tsx - 统一的股票图表组件KLineChart 实现)
import React, { useState } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Button,
ButtonGroup,
VStack,
HStack,
Text,
Badge,
Box,
Flex,
CircularProgress,
} from '@chakra-ui/react';
import RiskDisclaimer from '../RiskDisclaimer';
import { useKLineChart, useKLineData, useEventMarker } from './hooks';
import { Alert, AlertIcon } from '@chakra-ui/react';
/**
* 图表类型
*/
type ChartType = 'timeline' | 'daily';
/**
* 股票信息
*/
interface StockInfo {
stock_code: string;
stock_name?: string;
}
/**
* StockChartModal 组件 Props
*/
export interface StockChartModalProps {
/** 模态框是否打开 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 股票信息 */
stock: StockInfo | null;
/** 事件时间 */
eventTime?: string | null;
/** 是否使用 Chakra UI保留字段当前未使用 */
isChakraUI?: boolean;
/** 模态框大小 */
size?: string;
/** 初始图表类型 */
initialChartType?: ChartType;
}
const StockChartModal: React.FC<StockChartModalProps> = ({
isOpen,
onClose,
stock,
eventTime,
isChakraUI = true,
size = '6xl',
initialChartType = 'timeline',
}) => {
// 状态管理
const [chartType, setChartType] = useState<ChartType>(initialChartType);
// KLineChart Hooks
const { chart, chartRef, isInitialized, error: chartError } = useKLineChart({
containerId: `kline-chart-${stock?.stock_code || 'default'}`,
height: 500,
autoResize: true,
chartType, // ✅ 传递 chartType让 Hook 根据类型应用不同样式
});
const { data, loading, error: dataError } = useKLineData({
chart,
stockCode: stock?.stock_code || '',
chartType,
eventTime: eventTime || undefined,
autoLoad: true, // 改为 true让 Hook 内部根据 stockCode 和 chart 判断是否加载
});
const { marker } = useEventMarker({
chart,
data,
eventTime: eventTime || undefined,
eventTitle: '事件发生',
autoCreate: true,
});
// 守卫子句
if (!stock) return null;
return (
<Modal isOpen={isOpen} onClose={onClose} size={size}>
<ModalOverlay />
<ModalContent maxW="90vw" maxH="90vh" overflow="hidden">
<ModalHeader pb={4} position="relative">
<VStack align="flex-start" spacing={2}>
<HStack>
<Text fontSize="lg" fontWeight="bold">
{stock.stock_name || stock.stock_code} ({stock.stock_code}) -
</Text>
{data.length > 0 && <Badge colorScheme="blue">: {data.length}</Badge>}
</HStack>
<ButtonGroup size="sm">
<Button
variant={chartType === 'timeline' ? 'solid' : 'outline'}
onClick={() => setChartType('timeline')}
colorScheme="blue"
>
线
</Button>
<Button
variant={chartType === 'daily' ? 'solid' : 'outline'}
onClick={() => setChartType('daily')}
colorScheme="blue"
>
K线
</Button>
</ButtonGroup>
</VStack>
{/* 重件发生标签 - 仅在有 eventTime 时显示 */}
{eventTime && (
<Badge
colorScheme="yellow"
fontSize="sm"
px={3}
py={1}
borderRadius="md"
position="absolute"
top="4"
right="12"
boxShadow="sm"
>
()
</Badge>
)}
</ModalHeader>
<ModalCloseButton />
<ModalBody p={0} overflowY="auto" maxH="calc(90vh - 120px)">
{/* 错误提示 */}
{(chartError || dataError) && (
<Alert status="error" mx={4} mt={4}>
<AlertIcon />
{chartError?.message || dataError?.message}
</Alert>
)}
{/* 图表区域 - 响应式高度 */}
<Box
h={{
base: "calc(60vh - 100px)", // 移动端60% 视口高度 - 100px
md: "calc(70vh - 150px)", // 平板70% 视口高度 - 150px
lg: "calc(80vh - 200px)" // 桌面80% 视口高度 - 200px
}}
minH="350px" // 最小高度:确保可用性
maxH="650px" // 最大高度:避免过大
w="100%"
position="relative"
>
{loading && (
<Flex
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
bg="rgba(255, 255, 255, 0.7)"
zIndex="10"
alignItems="center"
justifyContent="center"
>
<VStack spacing={4}>
<CircularProgress isIndeterminate color="blue.300" />
<Text>...</Text>
</VStack>
</Flex>
)}
<div
ref={chartRef}
id={`kline-chart-${stock.stock_code}`}
style={{ width: '100%', height: '100%' }}
/>
</Box>
{/* 风险提示 */}
<Box px={4} pb={4}>
<RiskDisclaimer text="" variant="default" sx={{}} />
</Box>
</ModalBody>
</ModalContent>
</Modal>
);
};
export default StockChartModal;

View File

@@ -0,0 +1,514 @@
// src/components/StockChart/TimelineChartModal.tsx - 分时图弹窗组件
import React, { useEffect, useRef, useState } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
VStack,
HStack,
Text,
Box,
Flex,
CircularProgress,
Alert,
AlertIcon,
} from '@chakra-ui/react';
import * as echarts from 'echarts';
import { stockService } from '@services/eventService';
import { logger } from '@utils/logger';
/**
* 股票信息
*/
interface StockInfo {
stock_code: string;
stock_name?: string;
}
/**
* TimelineChartModal 组件 Props
*/
export interface TimelineChartModalProps {
/** 模态框是否打开 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 股票信息 */
stock: StockInfo | null;
/** 事件时间 */
eventTime?: string | null;
/** 模态框大小 */
size?: string;
}
/**
* 分时图数据点
*/
interface TimelineDataPoint {
time: string;
price: number;
avg_price: number;
volume: number;
change_percent: number;
}
const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
isOpen,
onClose,
stock,
eventTime,
size = '5xl',
}) => {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<TimelineDataPoint[]>([]);
// 加载分时图数据
const loadData = async () => {
if (!stock?.stock_code) return;
setLoading(true);
setError(null);
try {
logger.debug('TimelineChartModal', '开始加载分时图数据 (loadData)', {
stockCode: stock.stock_code,
eventTime,
});
const response = await stockService.getKlineData(
stock.stock_code,
'timeline',
eventTime || undefined
);
console.log('[TimelineChartModal] API响应:', response);
if (!response || !response.data || response.data.length === 0) {
throw new Error('暂无分时数据');
}
console.log('[TimelineChartModal] 数据条数:', response.data.length);
setData(response.data);
logger.info('TimelineChartModal', '分时图数据加载成功 (loadData)', {
dataCount: response.data.length,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : '数据加载失败';
logger.error('TimelineChartModal', 'loadData', err as Error);
setError(errorMsg);
} finally {
setLoading(false);
}
};
// 初始化图表
useEffect(() => {
if (!isOpen) return;
// 延迟初始化,确保 Modal 动画完成后 DOM 已经渲染
const timer = setTimeout(() => {
if (!chartRef.current) {
console.error('[TimelineChartModal] DOM元素未找到无法初始化图表');
return;
}
console.log('[TimelineChartModal] 初始化图表...');
// 创建图表实例不使用主题直接在option中配置背景色
chartInstance.current = echarts.init(chartRef.current);
console.log('[TimelineChartModal] 图表实例创建成功');
// 监听窗口大小变化
const handleResize = () => {
chartInstance.current?.resize();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, 100); // 延迟100ms等待Modal完全打开
return () => {
clearTimeout(timer);
if (chartInstance.current) {
chartInstance.current.dispose();
chartInstance.current = null;
}
};
}, [isOpen]);
// 更新图表数据
useEffect(() => {
if (data.length === 0) {
console.log('[TimelineChartModal] 无数据,跳过图表更新');
return;
}
// 如果图表还没初始化等待200ms后重试给图表初始化留出时间
const updateChart = () => {
if (!chartInstance.current) {
console.warn('[TimelineChartModal] 图表实例不存在');
return false;
}
console.log('[TimelineChartModal] 开始更新图表,数据点:', data.length);
const times = data.map((d) => d.time);
const prices = data.map((d) => d.price);
const avgPrices = data.map((d) => d.avg_price);
const volumes = data.map((d) => d.volume);
// 计算涨跌颜色
const basePrice = data[0]?.price || 0;
const volumeColors = data.map((d) =>
d.price >= basePrice ? '#ef5350' : '#26a69a'
);
// 提取事件发生时间HH:MM格式
let eventTimeStr: string | null = null;
if (eventTime) {
try {
const eventDate = new Date(eventTime);
const hours = eventDate.getHours().toString().padStart(2, '0');
const minutes = eventDate.getMinutes().toString().padStart(2, '0');
eventTimeStr = `${hours}:${minutes}`;
console.log('[TimelineChartModal] 事件发生时间:', eventTimeStr);
} catch (e) {
console.error('[TimelineChartModal] 解析事件时间失败:', e);
}
}
// 图表配置
const option: echarts.EChartsOption = {
backgroundColor: '#1a1a1a',
title: {
text: `${stock?.stock_name || stock?.stock_code} - 分时图`,
left: 'center',
top: 10,
textStyle: {
color: '#e0e0e0',
fontSize: 18,
fontWeight: 'bold',
},
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(30, 30, 30, 0.95)',
borderColor: '#404040',
borderWidth: 1,
textStyle: {
color: '#e0e0e0',
},
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999',
},
},
formatter: (params: any) => {
const dataIndex = params[0]?.dataIndex;
if (dataIndex === undefined) return '';
const item = data[dataIndex];
const changeColor = item.change_percent >= 0 ? '#ef5350' : '#26a69a';
const changeSign = item.change_percent >= 0 ? '+' : '';
return `
<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 8px;">${item.time}</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>价格:</span>
<span style="color: ${changeColor}; font-weight: bold; margin-left: 20px;">${item.price.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>均价:</span>
<span style="color: #ffa726; margin-left: 20px;">${item.avg_price.toFixed(2)}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>涨跌幅:</span>
<span style="color: ${changeColor}; margin-left: 20px;">${changeSign}${item.change_percent.toFixed(2)}%</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>成交量:</span>
<span style="margin-left: 20px;">${(item.volume / 100).toFixed(0)}手</span>
</div>
</div>
`;
},
},
grid: [
{
left: '5%',
right: '5%',
top: '15%',
height: '55%',
},
{
left: '5%',
right: '5%',
top: '75%',
height: '15%',
},
],
xAxis: [
{
type: 'category',
data: times,
gridIndex: 0,
axisLine: {
lineStyle: {
color: '#404040',
},
},
axisLabel: {
color: '#999',
interval: Math.floor(times.length / 6),
},
splitLine: {
show: true,
lineStyle: {
color: '#2a2a2a',
},
},
},
{
type: 'category',
data: times,
gridIndex: 1,
axisLine: {
lineStyle: {
color: '#404040',
},
},
axisLabel: {
color: '#999',
interval: Math.floor(times.length / 6),
},
},
],
yAxis: [
{
scale: true,
gridIndex: 0,
splitLine: {
show: true,
lineStyle: {
color: '#2a2a2a',
},
},
axisLine: {
lineStyle: {
color: '#404040',
},
},
axisLabel: {
color: '#999',
formatter: (value: number) => value.toFixed(2),
},
},
{
scale: true,
gridIndex: 1,
splitLine: {
show: false,
},
axisLine: {
lineStyle: {
color: '#404040',
},
},
axisLabel: {
color: '#999',
formatter: (value: number) => {
if (value >= 10000) {
return (value / 10000).toFixed(1) + '万';
}
return value.toFixed(0);
},
},
},
],
series: [
{
name: '价格',
type: 'line',
data: prices,
xAxisIndex: 0,
yAxisIndex: 0,
smooth: true,
symbol: 'none',
lineStyle: {
color: '#2196f3',
width: 2,
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(33, 150, 243, 0.3)' },
{ offset: 1, color: 'rgba(33, 150, 243, 0.05)' },
]),
},
markLine: eventTimeStr ? {
silent: false,
symbol: 'none',
label: {
show: true,
position: 'insideEndTop',
formatter: '事件发生',
color: '#ffd700',
fontSize: 12,
fontWeight: 'bold',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
padding: [4, 8],
borderRadius: 4,
},
lineStyle: {
color: '#ffd700',
width: 2,
type: 'solid',
},
data: [
{
xAxis: eventTimeStr,
label: {
formatter: '⚡ 事件发生',
},
},
],
} : undefined,
},
{
name: '均价',
type: 'line',
data: avgPrices,
xAxisIndex: 0,
yAxisIndex: 0,
smooth: true,
symbol: 'none',
lineStyle: {
color: '#ffa726',
width: 1.5,
type: 'dashed',
},
},
{
name: '成交量',
type: 'bar',
data: volumes,
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: (params: any) => {
return volumeColors[params.dataIndex];
},
},
},
],
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1],
start: 0,
end: 100,
},
],
};
chartInstance.current.setOption(option);
console.log('[TimelineChartModal] 图表option已设置');
// 强制resize以确保图表正确显示
setTimeout(() => {
chartInstance.current?.resize();
console.log('[TimelineChartModal] 图表已resize');
}, 100);
return true;
};
// 立即尝试更新,如果失败则重试
if (!updateChart()) {
console.log('[TimelineChartModal] 第一次更新失败200ms后重试...');
const retryTimer = setTimeout(() => {
updateChart();
}, 200);
return () => clearTimeout(retryTimer);
}
}, [data, stock]);
// 加载数据
useEffect(() => {
if (isOpen) {
loadData();
}
}, [isOpen, stock?.stock_code, eventTime]);
if (!stock) return null;
return (
<Modal isOpen={isOpen} onClose={onClose} size={size}>
<ModalOverlay bg="blackAlpha.700" />
<ModalContent
maxW="90vw"
maxH="85vh"
bg="#1a1a1a"
borderColor="#404040"
borderWidth="1px"
>
<ModalHeader pb={3} borderBottomWidth="1px" borderColor="#404040">
<VStack align="flex-start" spacing={1}>
<HStack>
<Text fontSize="lg" fontWeight="bold" color="#e0e0e0">
{stock.stock_name || stock.stock_code} ({stock.stock_code})
</Text>
</HStack>
<Text fontSize="sm" color="#999">
</Text>
</VStack>
</ModalHeader>
<ModalCloseButton color="#999" _hover={{ color: '#e0e0e0' }} />
<ModalBody p={4}>
{error && (
<Alert status="error" bg="#2a1a1a" borderColor="#ef5350" mb={4}>
<AlertIcon color="#ef5350" />
<Text color="#e0e0e0">{error}</Text>
</Alert>
)}
<Box position="relative" h="600px" w="100%">
{loading && (
<Flex
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
bg="rgba(26, 26, 26, 0.7)"
zIndex="10"
alignItems="center"
justifyContent="center"
>
<VStack spacing={4}>
<CircularProgress isIndeterminate color="blue.400" />
<Text color="#e0e0e0">...</Text>
</VStack>
</Flex>
)}
<div ref={chartRef} style={{ width: '100%', height: '100%' }} />
</Box>
</ModalBody>
</ModalContent>
</Modal>
);
};
export default TimelineChartModal;

View File

@@ -0,0 +1,205 @@
/**
* KLineChart 图表常量配置
*
* 包含图表默认配置、技术指标列表、事件标记配置等
*/
import type { ChartConfig, ChartType } from '../types';
/**
* 图表默认高度px
*/
export const CHART_HEIGHTS = {
/** 主图高度 */
main: 400,
/** 副图高度(技术指标) */
sub: 150,
/** 移动端主图高度 */
mainMobile: 300,
/** 移动端副图高度 */
subMobile: 100,
} as const;
/**
* 技术指标配置
*/
export const INDICATORS = {
/** 主图指标(叠加在 K 线图上) */
main: [
{
name: 'MA',
label: '均线',
params: [5, 10, 20, 30],
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A'],
},
{
name: 'EMA',
label: '指数移动平均',
params: [5, 10, 20, 30],
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A'],
},
{
name: 'BOLL',
label: '布林带',
params: [20, 2],
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1'],
},
],
/** 副图指标(单独窗口显示) */
sub: [
{
name: 'VOL',
label: '成交量',
params: [5, 10, 20],
colors: ['#ef5350', '#26a69a'],
},
{
name: 'MACD',
label: 'MACD',
params: [12, 26, 9],
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1'],
},
{
name: 'KDJ',
label: 'KDJ',
params: [9, 3, 3],
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1'],
},
{
name: 'RSI',
label: 'RSI',
params: [6, 12, 24],
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1'],
},
],
} as const;
/**
* 默认主图指标(初始显示)
*/
export const DEFAULT_MAIN_INDICATOR = 'MA';
/**
* 默认副图指标(初始显示)
*/
export const DEFAULT_SUB_INDICATORS = ['VOL', 'MACD'];
/**
* 图表类型配置
*/
export const CHART_TYPE_CONFIG: Record<ChartType, { label: string; dateFormat: string }> = {
timeline: {
label: '分时图',
dateFormat: 'HH:mm', // 时间格式09:30
},
daily: {
label: '日K线',
dateFormat: 'YYYY-MM-DD', // 日期格式2024-01-01
},
} as const;
/**
* 事件标记配置
*/
export const EVENT_MARKER_CONFIG = {
/** 默认颜色 */
defaultColor: '#ff9800',
/** 默认位置 */
defaultPosition: 'top' as const,
/** 默认图标 */
defaultIcon: '📌',
/** 标记大小 */
size: {
point: 8, // 标记点半径
icon: 20, // 图标大小
},
/** 文本配置 */
text: {
fontSize: 12,
fontFamily: 'Helvetica, Arial, sans-serif',
color: '#ffffff',
padding: 4,
borderRadius: 4,
},
} as const;
/**
* 数据加载配置
*/
export const DATA_LOADER_CONFIG = {
/** 最大数据点数(避免性能问题) */
maxDataPoints: 1000,
/** 初始加载数据点数 */
initialLoadCount: 100,
/** 加载更多时的数据点数 */
loadMoreCount: 50,
} as const;
/**
* 缩放配置
*/
export const ZOOM_CONFIG = {
/** 最小缩放比例(显示更多 K 线) */
minZoom: 0.5,
/** 最大缩放比例(显示更少 K 线) */
maxZoom: 2.0,
/** 默认缩放比例 */
defaultZoom: 1.0,
/** 缩放步长 */
zoomStep: 0.1,
} as const;
/**
* 默认图表配置
*/
export const DEFAULT_CHART_CONFIG: ChartConfig = {
type: 'daily',
showIndicators: true,
defaultIndicators: DEFAULT_SUB_INDICATORS,
height: CHART_HEIGHTS.main,
showGrid: true,
showCrosshair: true,
} as const;
/**
* 图表初始化选项(传递给 KLineChart.init
*/
export const CHART_INIT_OPTIONS = {
/** 时区(中国标准时间) */
timezone: 'Asia/Shanghai',
/** 语言 */
locale: 'zh-CN',
/** 自定义配置 */
customApi: {
formatDate: (timestamp: number, format: string) => {
// 可在此处自定义日期格式化逻辑
return new Date(timestamp).toLocaleString('zh-CN');
},
},
} as const;
/**
* 分时图特殊配置
*/
export const TIMELINE_CONFIG = {
/** 交易时段A 股) */
tradingSessions: [
{ start: '09:30', end: '11:30' }, // 上午
{ start: '13:00', end: '15:00' }, // 下午
],
/** 是否显示均价线 */
showAverageLine: true,
/** 均价线颜色 */
averageLineColor: '#FFB74D',
} as const;
/**
* 日K线特殊配置
*/
export const DAILY_KLINE_CONFIG = {
/** 最大显示天数 */
maxDays: 250, // 约一年交易日
/** 默认显示天数 */
defaultDays: 60,
} as const;

View File

@@ -0,0 +1,32 @@
/**
* StockChart 配置统一导出
*
* 使用方式:
* import { lightTheme, DEFAULT_CHART_CONFIG } from '@components/StockChart/config';
*/
// 主题配置(仅浅色主题)
export {
CHART_COLORS,
lightTheme,
// darkTheme, // ❌ 已删除深色主题
timelineTheme,
getTheme,
getTimelineTheme,
} from './klineTheme';
// 图表配置
export {
CHART_HEIGHTS,
INDICATORS,
DEFAULT_MAIN_INDICATOR,
DEFAULT_SUB_INDICATORS,
CHART_TYPE_CONFIG,
EVENT_MARKER_CONFIG,
DATA_LOADER_CONFIG,
ZOOM_CONFIG,
DEFAULT_CHART_CONFIG,
CHART_INIT_OPTIONS,
TIMELINE_CONFIG,
DAILY_KLINE_CONFIG,
} from './chartConfig';

View File

@@ -0,0 +1,370 @@
/**
* KLineChart 主题配置(仅浅色主题)
*
* 适配 klinecharts@10.0.0-beta1
* 参考: https://github.com/klinecharts/KLineChart/blob/main/docs/en-US/guide/styles.md
*
* ⚠️ 重要说明:
* - 本项目已移除深色模式支持2025-01
* - 应用通过 colorModeManager 强制使用浅色主题
* - 已删除 darkTheme 和 timelineThemeDark 配置
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
// ⚠️ 使用 any 类型绕过 KLineChart 类型定义的限制beta 版本类型不完整)
// import type { DeepPartial, Styles } from 'klinecharts'; // ⚠️ 未使用(保留以便将来扩展)
/**
* 图表主题颜色配置(浅色主题)
* ⚠️ 已移除深色模式相关颜色常量
*/
export const CHART_COLORS = {
// 涨跌颜色(中国市场习惯:红涨绿跌)
up: '#ef5350', // 上涨红色
down: '#26a69a', // 下跌绿色
neutral: '#888888', // 平盘灰色
// 主题色(继承自 Argon Dashboard
primary: '#1b3bbb', // Navy 500
secondary: '#728fea', // Navy 300
background: '#ffffff',
// 文本颜色
text: '#333333',
textSecondary: '#888888',
// 网格颜色
grid: '#e0e0e0',
// 边框颜色
border: '#e0e0e0',
// 事件标记颜色
eventMarker: '#ff9800',
eventMarkerText: '#ffffff',
};
/**
* 浅色主题配置(默认)
*/
export const lightTheme: any = {
candle: {
type: 'candle_solid', // 实心蜡烛图
bar: {
upColor: CHART_COLORS.up,
downColor: CHART_COLORS.down,
noChangeColor: CHART_COLORS.neutral,
},
priceMark: {
show: true,
high: {
color: CHART_COLORS.up,
},
low: {
color: CHART_COLORS.down,
},
},
tooltip: {
showRule: 'always',
showType: 'standard',
// labels: ['时间: ', '开: ', '收: ', '高: ', '低: ', '成交量: '], // ❌ KLineChart 类型不支持自定义 labels
text: {
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
color: CHART_COLORS.text,
},
},
},
indicator: {
tooltip: {
showRule: 'always',
showType: 'standard',
text: {
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
color: CHART_COLORS.text,
},
},
},
xAxis: {
axisLine: {
show: true,
color: CHART_COLORS.border,
},
tickLine: {
show: true,
length: 3,
color: CHART_COLORS.border,
},
tickText: {
show: true,
color: CHART_COLORS.textSecondary,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
size: 12,
},
},
yAxis: {
axisLine: {
show: true,
color: CHART_COLORS.border,
},
tickLine: {
show: true,
length: 3,
color: CHART_COLORS.border,
},
tickText: {
show: true,
color: CHART_COLORS.textSecondary,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
size: 12,
},
type: 'normal', // 'normal' | 'percentage' | 'log'
},
grid: {
show: true,
horizontal: {
show: true,
size: 1,
color: CHART_COLORS.grid,
style: 'dashed',
},
vertical: {
show: false, // 垂直网格线通常关闭,避免过于密集
},
},
separator: {
size: 1,
color: CHART_COLORS.border,
},
crosshair: {
show: true,
horizontal: {
show: true,
line: {
show: true,
style: 'dashed',
dashedValue: [4, 2], // ✅ 修复: 使用 dashedValue 而非 dashValue
size: 1,
color: CHART_COLORS.primary,
},
text: {
show: true,
color: '#ffffff', // 白色文字(十字线标签背景是深蓝色)
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
backgroundColor: CHART_COLORS.primary,
},
},
vertical: {
show: true,
line: {
show: true,
style: 'dashed',
dashedValue: [4, 2], // ✅ 修复: 使用 dashedValue 而非 dashValue
size: 1,
color: CHART_COLORS.primary,
},
text: {
show: true,
color: '#ffffff', // 白色文字(十字线标签背景是深蓝色)
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
backgroundColor: CHART_COLORS.primary,
},
},
},
overlay: {
// 事件标记覆盖层样式
point: {
color: CHART_COLORS.eventMarker,
borderColor: CHART_COLORS.eventMarker,
borderSize: 1,
radius: 5,
activeColor: CHART_COLORS.eventMarker,
activeBorderColor: CHART_COLORS.eventMarker,
activeBorderSize: 2,
activeRadius: 6,
},
line: {
style: 'solid',
smooth: false,
color: CHART_COLORS.eventMarker,
size: 1,
dashedValue: [2, 2],
},
text: {
style: 'fill',
color: CHART_COLORS.eventMarkerText,
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
offset: [0, 0],
},
rect: {
style: 'fill',
color: CHART_COLORS.eventMarker,
borderColor: CHART_COLORS.eventMarker,
borderSize: 1,
borderRadius: 4,
borderStyle: 'solid',
borderDashedValue: [2, 2],
},
},
};
// ❌ 已删除 darkTheme 配置(不再支持深色模式)
/**
* 分时图专用主题配置
* 特点面积图样式、均价线、百分比Y轴
*/
export const timelineTheme: any = {
...lightTheme,
candle: {
type: 'area', // ✅ 面积图模式(分时线)
area: {
lineSize: 2,
lineColor: CHART_COLORS.up, // 默认红色,实际会根据涨跌动态调整
value: 'close',
backgroundColor: [
{
offset: 0,
color: 'rgba(239, 83, 80, 0.2)', // 红色半透明渐变(顶部)
},
{
offset: 1,
color: 'rgba(239, 83, 80, 0.01)', // 红色几乎透明(底部)
},
],
},
priceMark: {
show: true,
high: {
show: false, // 分时图不显示最高最低价标记
},
low: {
show: false,
},
last: {
show: true,
upColor: CHART_COLORS.up,
downColor: CHART_COLORS.down,
noChangeColor: CHART_COLORS.neutral,
line: {
show: true,
style: 'dashed',
dashedValue: [4, 2], // ✅ 修复: 使用 dashedValue 而非 dashValue
size: 1,
},
text: {
show: true,
size: 12,
paddingLeft: 4,
paddingTop: 2,
paddingRight: 4,
paddingBottom: 2,
borderRadius: 2,
},
},
},
tooltip: {
showRule: 'always',
showType: 'standard',
// ❌ KLineChart 类型不支持自定义 labels 和 formatter需要在运行时通过 API 设置)
// labels: ['时间: ', '现价: ', '涨跌: ', '均价: ', '昨收: ', '成交量: '],
// formatter: (data: any, indicator: any) => { ... },
text: {
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
color: CHART_COLORS.text,
},
},
},
yAxis: {
...lightTheme.yAxis,
type: 'percentage', // ✅ 百分比模式
position: 'left', // Y轴在左侧
inside: false,
reverse: false,
tickText: {
...lightTheme.yAxis?.tickText,
// ❌ KLineChart 类型不支持自定义 formatter需要在运行时通过 API 设置)
// formatter: (value: any) => {
// const percent = (value * 100).toFixed(2);
// if (Math.abs(value) < 0.0001) return '0.00%';
// return value > 0 ? `+${percent}%` : `${percent}%`;
// },
},
},
grid: {
show: true,
horizontal: {
show: true,
size: 1,
color: CHART_COLORS.grid,
style: 'solid', // 分时图使用实线网格
},
vertical: {
show: false,
},
},
};
// ❌ 已删除 timelineThemeDark 配置(不再支持深色模式)
/**
* 获取主题配置(固定返回浅色主题)
* ⚠️ 已移除深色模式支持
* @deprecated colorMode 参数已废弃,始终返回浅色主题
*/
export const getTheme = (_colorMode?: 'light' | 'dark'): any => {
// ✅ 始终返回浅色主题
return lightTheme;
};
/**
* 获取分时图主题配置(固定返回浅色主题)
* ⚠️ 已移除深色模式支持
* @deprecated colorMode 参数已废弃,始终返回浅色主题
*/
export const getTimelineTheme = (_colorMode?: 'light' | 'dark'): any => {
// ✅ 始终使用浅色主题
const baseTheme = timelineTheme;
// ✅ 添加成交量指标样式(蓝色渐变柱状图)+ 成交量单位格式化
return {
...baseTheme,
indicator: {
...baseTheme.indicator,
bars: [
{
upColor: 'rgba(59, 130, 246, 0.6)', // 蓝色(涨)
downColor: 'rgba(59, 130, 246, 0.6)', // 蓝色(跌)- 分时图成交量统一蓝色
noChangeColor: 'rgba(59, 130, 246, 0.6)',
}
],
// ❌ KLineChart 类型不支持自定义 formatter需要在运行时通过 API 设置)
tooltip: {
...baseTheme.indicator?.tooltip,
// formatter: (params: any) => {
// if (params.name === 'VOL' && params.calcParamsText) {
// const volume = params.calcParamsText.match(/\d+/)?.[0];
// if (volume) {
// const hands = Math.floor(Number(volume) / 100);
// return `成交量: ${hands.toLocaleString()}手`;
// }
// }
// return params.calcParamsText || '';
// },
},
},
};
};

View File

@@ -0,0 +1,15 @@
/**
* StockChart 自定义 Hooks 统一导出
*
* 使用方式:
* import { useKLineChart, useKLineData, useEventMarker } from '@components/StockChart/hooks';
*/
export { useKLineChart } from './useKLineChart';
export type { UseKLineChartOptions, UseKLineChartReturn } from './useKLineChart';
export { useKLineData } from './useKLineData';
export type { UseKLineDataOptions, UseKLineDataReturn } from './useKLineData';
export { useEventMarker } from './useEventMarker';
export type { UseEventMarkerOptions, UseEventMarkerReturn } from './useEventMarker';

View File

@@ -0,0 +1,238 @@
/**
* useEventMarker Hook
*
* 管理事件标记的创建、更新和删除
*/
import { useEffect, useState, useCallback } from 'react';
import type { Chart } from 'klinecharts';
import type { EventMarker, KLineDataPoint } from '../types';
import {
createEventMarkerFromTime,
createEventMarkerOverlay,
createEventHighlightOverlay,
removeAllEventMarkers,
} from '../utils/eventMarkerUtils';
import { logger } from '@utils/logger';
export interface UseEventMarkerOptions {
/** KLineChart 实例 */
chart: Chart | null;
/** K 线数据(用于定位标记) */
data: KLineDataPoint[];
/** 事件时间ISO 字符串) */
eventTime?: string;
/** 事件标题(用于标记标签) */
eventTitle?: string;
/** 是否自动创建标记 */
autoCreate?: boolean;
}
export interface UseEventMarkerReturn {
/** 当前标记 */
marker: EventMarker | null;
/** 标记 ID已添加到图表 */
markerId: string | null;
/** 创建标记 */
createMarker: (time: string, label: string, color?: string) => void;
/** 移除标记 */
removeMarker: () => void;
/** 移除所有标记 */
removeAllMarkers: () => void;
}
/**
* 事件标记管理 Hook
*
* @param options 配置选项
* @returns UseEventMarkerReturn
*
* @example
* const { marker, createMarker, removeMarker } = useEventMarker({
* chart,
* data,
* eventTime: '2024-01-01 10:00:00',
* eventTitle: '重大公告',
* autoCreate: true,
* });
*/
export const useEventMarker = (
options: UseEventMarkerOptions
): UseEventMarkerReturn => {
const {
chart,
data,
eventTime,
eventTitle = '事件发生',
autoCreate = true,
} = options;
const [marker, setMarker] = useState<EventMarker | null>(null);
const [markerId, setMarkerId] = useState<string | null>(null);
const [highlightId, setHighlightId] = useState<string | null>(null);
/**
* 创建事件标记
*/
const createMarker = useCallback(
(time: string, label: string, color?: string) => {
if (!chart || !data || data.length === 0) {
logger.warn('useEventMarker', '图表或数据未准备好 (createMarker)', {
hasChart: !!chart,
dataLength: data?.length || 0,
});
return;
}
try {
// 1. 创建事件标记配置
const eventMarker = createEventMarkerFromTime(time, label, color);
setMarker(eventMarker);
// 2. 创建 Overlay
const overlay = createEventMarkerOverlay(eventMarker, data);
if (!overlay) {
logger.warn('useEventMarker', 'Overlay 创建失败 (createMarker)', {
eventMarker,
});
return;
}
// 3. 添加到图表
const id = chart.createOverlay(overlay);
if (!id || (Array.isArray(id) && id.length === 0)) {
logger.warn('useEventMarker', '标记添加失败 (createMarker)', {
overlay,
});
return;
}
const actualId = Array.isArray(id) ? id[0] : id;
setMarkerId(actualId as string);
// 4. 创建黄色高亮背景(事件影响日)
const highlightOverlay = createEventHighlightOverlay(time, data);
if (highlightOverlay) {
const highlightResult = chart.createOverlay(highlightOverlay);
const actualHighlightId = Array.isArray(highlightResult) ? highlightResult[0] : highlightResult;
setHighlightId(actualHighlightId as string);
logger.info('useEventMarker', '事件高亮背景创建成功 (createMarker)', {
highlightId: actualHighlightId,
});
}
logger.info('useEventMarker', '事件标记创建成功 (createMarker)', {
markerId: actualId,
label,
time,
chartId: chart.id,
});
} catch (err) {
logger.error('useEventMarker', 'createMarker', err as Error, {
time,
label,
});
}
},
[chart, data]
);
/**
* 移除事件标记
*/
const removeMarker = useCallback(() => {
if (!chart) {
return;
}
try {
if (markerId) {
chart.removeOverlay(markerId);
}
if (highlightId) {
chart.removeOverlay(highlightId);
}
setMarker(null);
setMarkerId(null);
setHighlightId(null);
logger.debug('useEventMarker', '移除事件标记和高亮 (removeMarker)', {
markerId,
highlightId,
chartId: chart.id,
});
} catch (err) {
logger.error('useEventMarker', 'removeMarker', err as Error, {
markerId,
highlightId,
});
}
}, [chart, markerId, highlightId]);
/**
* 移除所有标记
*/
const removeAllMarkers = useCallback(() => {
if (!chart) {
return;
}
try {
removeAllEventMarkers(chart);
setMarker(null);
setMarkerId(null);
setHighlightId(null);
logger.debug('useEventMarker', '移除所有事件标记和高亮 (removeAllMarkers)', {
chartId: chart.id,
});
} catch (err) {
logger.error('useEventMarker', 'removeAllMarkers', err as Error);
}
}, [chart]);
// 自动创建标记(当 eventTime 和数据都准备好时)
useEffect(() => {
if (
autoCreate &&
eventTime &&
chart &&
data &&
data.length > 0 &&
!markerId // 避免重复创建
) {
createMarker(eventTime, eventTitle);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventTime, chart, data, autoCreate]);
// 清理:组件卸载时移除所有标记
useEffect(() => {
return () => {
if (chart) {
try {
if (markerId) {
chart.removeOverlay(markerId);
}
if (highlightId) {
chart.removeOverlay(highlightId);
}
} catch (err) {
// 忽略清理时的错误
}
}
};
}, [chart, markerId, highlightId]);
return {
marker,
markerId,
createMarker,
removeMarker,
removeAllMarkers,
};
};

View File

@@ -0,0 +1,247 @@
/**
* useKLineChart Hook
*
* 管理 KLineChart 实例的初始化、配置和销毁
*/
import { useEffect, useRef, useState } from 'react';
import { init, dispose, registerIndicator } from 'klinecharts';
import type { Chart } from 'klinecharts';
// import { useColorMode } from '@chakra-ui/react'; // ❌ 已移除深色模式支持
import { getTheme, getTimelineTheme } from '../config/klineTheme';
import { CHART_INIT_OPTIONS } from '../config';
import { logger } from '@utils/logger';
import { avgPriceIndicator } from '../indicators/avgPriceIndicator';
export interface UseKLineChartOptions {
/** 图表容器 ID */
containerId: string;
/** 图表高度px */
height?: number;
/** 是否自动调整大小 */
autoResize?: boolean;
/** 图表类型timeline/daily */
chartType?: 'timeline' | 'daily';
}
export interface UseKLineChartReturn {
/** KLineChart 实例 */
chart: Chart | null;
/** 容器 Ref */
chartRef: React.RefObject<HTMLDivElement>;
/** 是否已初始化 */
isInitialized: boolean;
/** 初始化错误 */
error: Error | null;
}
/**
* KLineChart 初始化和生命周期管理 Hook
*
* @param options 配置选项
* @returns UseKLineChartReturn
*
* @example
* const { chart, chartRef, isInitialized } = useKLineChart({
* containerId: 'kline-chart',
* height: 400,
* autoResize: true,
* });
*/
export const useKLineChart = (
options: UseKLineChartOptions
): UseKLineChartReturn => {
const { containerId, height = 400, autoResize = true, chartType = 'daily' } = options;
const chartRef = useRef<HTMLDivElement>(null);
const chartInstanceRef = useRef<Chart | null>(null);
const [chartInstance, setChartInstance] = useState<Chart | null>(null); // ✅ 新增chart state触发重渲染
const [isInitialized, setIsInitialized] = useState(false);
const [error, setError] = useState<Error | null>(null);
// ✅ 固定使用浅色主题(已移除 useColorMode
const colorMode = 'light';
// 全局注册自定义均价线指标(只执行一次)
useEffect(() => {
try {
registerIndicator(avgPriceIndicator);
logger.debug('useKLineChart', '✅ 自定义均价线指标(AVG)注册成功');
} catch (err) {
// 如果已注册会报错,忽略即可
logger.debug('useKLineChart', 'AVG指标已注册或注册失败', err);
}
}, []);
// 图表初始化(添加延迟重试机制,处理 Modal 动画延迟)
useEffect(() => {
// 图表初始化函数
const initChart = (): boolean => {
if (!chartRef.current) {
logger.warn('useKLineChart', '图表容器未挂载,将在 50ms 后重试 (init)', { containerId });
return false;
}
try {
logger.debug('useKLineChart', '开始初始化图表 (init)', {
containerId,
height,
colorMode,
});
// 初始化图表实例KLineChart 10.0 API
// ✅ 根据 chartType 选择主题
const themeStyles = chartType === 'timeline'
? getTimelineTheme(colorMode)
: getTheme(colorMode);
const chartInstance = init(chartRef.current, {
...CHART_INIT_OPTIONS,
// 设置初始样式(根据主题和图表类型)
styles: themeStyles,
});
if (!chartInstance) {
throw new Error('图表初始化失败:返回 null');
}
chartInstanceRef.current = chartInstance;
setChartInstance(chartInstance); // ✅ 新增:更新 state触发重渲染
setIsInitialized(true);
setError(null);
// ✅ 新增:创建成交量指标窗格
try {
const volumePaneId = chartInstance.createIndicator('VOL', false, {
height: 100, // 固定高度 100px约占整体的 20-25%
});
logger.debug('useKLineChart', '成交量窗格创建成功 (init)', {
volumePaneId,
});
} catch (err) {
logger.warn('useKLineChart', '成交量窗格创建失败 (init)', {
error: err,
});
// 不阻塞主流程,继续执行
}
logger.info('useKLineChart', '✅ 图表初始化成功 (init)', {
containerId,
chartId: chartInstance.id,
});
return true;
} catch (err) {
const error = err as Error;
logger.error('useKLineChart', 'init', error, { containerId });
setError(error);
setIsInitialized(false);
return false;
}
};
// 立即尝试初始化
if (initChart()) {
// 成功,直接返回清理函数
return () => {
if (chartInstanceRef.current) {
logger.debug('useKLineChart', '销毁图表实例 (dispose)', {
containerId,
chartId: chartInstanceRef.current.id,
});
dispose(chartInstanceRef.current);
chartInstanceRef.current = null;
setChartInstance(null); // ✅ 新增:清空 state
setIsInitialized(false);
}
};
}
// 失败则延迟重试(处理 Modal 动画延迟导致的 DOM 未挂载)
const timer = setTimeout(() => {
logger.debug('useKLineChart', '执行延迟重试 (init)', { containerId });
initChart();
}, 50);
// 清理函数:清除定时器和销毁图表实例
return () => {
clearTimeout(timer);
if (chartInstanceRef.current) {
logger.debug('useKLineChart', '销毁图表实例 (dispose)', {
containerId,
chartId: chartInstanceRef.current.id,
});
dispose(chartInstanceRef.current);
chartInstanceRef.current = null;
setChartInstance(null); // ✅ 新增:清空 state
setIsInitialized(false);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerId, chartType]); // containerId 或 chartType 变化时重新初始化
// 主题切换:更新图表样式
useEffect(() => {
if (!chartInstanceRef.current || !isInitialized) {
return;
}
try {
// ✅ 根据 chartType 选择主题
const newTheme = chartType === 'timeline'
? getTimelineTheme(colorMode)
: getTheme(colorMode);
chartInstanceRef.current.setStyles(newTheme);
logger.debug('useKLineChart', '更新图表主题 (updateTheme)', {
colorMode,
chartType,
chartId: chartInstanceRef.current.id,
});
} catch (err) {
logger.error('useKLineChart', 'updateTheme', err as Error, { colorMode, chartType });
}
}, [colorMode, chartType, isInitialized]);
// 容器尺寸变化:调整图表大小
useEffect(() => {
if (!chartInstanceRef.current || !isInitialized || !autoResize) {
return;
}
const handleResize = () => {
if (chartInstanceRef.current) {
chartInstanceRef.current.resize();
logger.debug('useKLineChart', 'resize', '调整图表大小');
}
};
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
// 使用 ResizeObserver 监听容器大小变化(更精确)
let resizeObserver: ResizeObserver | null = null;
if (chartRef.current && typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(chartRef.current);
}
return () => {
window.removeEventListener('resize', handleResize);
if (resizeObserver && chartRef.current) {
resizeObserver.unobserve(chartRef.current);
resizeObserver.disconnect();
}
};
}, [isInitialized, autoResize]);
return {
chart: chartInstance, // ✅ 返回 state 而非 ref确保变化触发重渲染
chartRef,
isInitialized,
error,
};
};

View File

@@ -0,0 +1,329 @@
/**
* useKLineData Hook
*
* 管理 K 线数据的加载、转换和更新
*/
import { useEffect, useState, useCallback } from 'react';
import type { Chart } from 'klinecharts';
import type { ChartType, KLineDataPoint, RawDataPoint } from '../types';
import { processChartData } from '../utils/dataAdapter';
import { logger } from '@utils/logger';
import { stockService } from '@services/eventService';
import { klineDataCache, getCacheKey } from '@views/Community/components/StockDetailPanel/utils/klineDataCache';
export interface UseKLineDataOptions {
/** KLineChart 实例 */
chart: Chart | null;
/** 股票代码 */
stockCode: string;
/** 图表类型 */
chartType: ChartType;
/** 事件时间(用于调整数据加载范围) */
eventTime?: string;
/** 是否自动加载数据 */
autoLoad?: boolean;
}
export interface UseKLineDataReturn {
/** 处理后的 K 线数据 */
data: KLineDataPoint[];
/** 原始数据 */
rawData: RawDataPoint[];
/** 是否加载中 */
loading: boolean;
/** 加载错误 */
error: Error | null;
/** 手动加载数据 */
loadData: () => Promise<void>;
/** 更新数据 */
updateData: (newData: KLineDataPoint[]) => void;
/** 清空数据 */
clearData: () => void;
}
/**
* K 线数据加载和管理 Hook
*
* @param options 配置选项
* @returns UseKLineDataReturn
*
* @example
* const { data, loading, error, loadData } = useKLineData({
* chart,
* stockCode: '600000.SH',
* chartType: 'daily',
* eventTime: '2024-01-01 10:00:00',
* autoLoad: true,
* });
*/
export const useKLineData = (
options: UseKLineDataOptions
): UseKLineDataReturn => {
const {
chart,
stockCode,
chartType,
eventTime,
autoLoad = true,
} = options;
const [data, setData] = useState<KLineDataPoint[]>([]);
const [rawData, setRawData] = useState<RawDataPoint[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
/**
* 加载数据(从后端 API
*/
const loadData = useCallback(async () => {
if (!stockCode) {
logger.warn('useKLineData', '股票代码为空 (loadData)', { chartType });
return;
}
setLoading(true);
setError(null);
try {
logger.debug('useKLineData', '开始加载数据 (loadData)', {
stockCode,
chartType,
eventTime,
});
// 1. 先检查缓存
const cacheKey = getCacheKey(stockCode, eventTime, chartType);
const cachedData = klineDataCache.get(cacheKey);
let rawDataList;
if (cachedData && cachedData.length > 0) {
// 使用缓存数据
rawDataList = cachedData;
} else {
// 2. 缓存没有数据,调用 API 请求
const response = await stockService.getKlineData(
stockCode,
chartType,
eventTime
);
if (!response || !response.data) {
throw new Error('后端返回数据为空');
}
rawDataList = response.data;
// 3. 将数据写入缓存(避免下次重复请求)
klineDataCache.set(cacheKey, rawDataList);
}
setRawData(rawDataList);
// 数据转换和处理
const processedData = processChartData(rawDataList, chartType, eventTime);
setData(processedData);
logger.info('useKLineData', '数据加载成功 (loadData)', {
stockCode,
chartType,
rawCount: rawDataList.length,
processedCount: processedData.length,
});
} catch (err) {
const error = err as Error;
logger.error('useKLineData', 'loadData', error, {
stockCode,
chartType,
});
setError(error);
setData([]);
setRawData([]);
} finally {
setLoading(false);
}
}, [stockCode, chartType, eventTime]);
/**
* 更新图表数据(使用 setDataLoader 方法)
*/
const updateChartData = useCallback(
(klineData: KLineDataPoint[]) => {
if (!chart || klineData.length === 0) {
return;
}
try {
// 步骤 1: 设置 symbol必需getBars 调用的前置条件)
(chart as any).setSymbol({
ticker: stockCode || 'UNKNOWN', // 股票代码
pricePrecision: 2, // 价格精度2位小数
volumePrecision: 0 // 成交量精度(整数)
});
// 步骤 2: 设置 period必需getBars 调用的前置条件)
const periodType = chartType === 'timeline' ? 'minute' : 'day';
(chart as any).setPeriod({
type: periodType, // 分时图=minute日K=day
span: 1 // 周期跨度1分钟/1天
});
// 步骤 3: 设置 DataLoader同步数据加载器
(chart as any).setDataLoader({
getBars: (params: any) => {
if (params.type === 'init') {
// 初始化加载:返回完整数据
params.callback(klineData, false); // false = 无更多数据可加载
} else if (params.type === 'forward' || params.type === 'backward') {
// 向前/向后加载:我们没有更多数据,返回空数组
params.callback([], false);
}
}
});
// 步骤 4: 触发初始化加载(这会调用 getBars with type="init"
(chart as any).resetData();
// 步骤 5: 根据数据量调整可见范围和柱子间距(让 K 线柱子填满图表区域)
setTimeout(() => {
try {
const dataLength = klineData.length;
if (dataLength > 0) {
// 获取图表容器宽度
const chartDom = (chart as any).getDom();
const chartWidth = chartDom?.clientWidth || 1200;
// 计算最优柱子间距
// 公式barSpace = (图表宽度 / 数据数量) * 0.7
// 0.7 是为了留出一些间距,让图表不会太拥挤
const optimalBarSpace = Math.max(8, Math.min(50, (chartWidth / dataLength) * 0.7));
(chart as any).setBarSpace(optimalBarSpace);
// 减少右侧空白(默认值可能是 100-200调小会减少右侧空白
(chart as any).setOffsetRightDistance(50);
}
} catch (err) {
logger.error('useKLineData', 'updateChartData', err as Error, {
step: '调整可见范围失败',
});
}
}, 100); // 延迟 100ms 确保数据已加载和渲染
// ✅ 步骤 4: 分时图添加均价线(使用自定义 AVG 指标)
if (chartType === 'timeline' && klineData.length > 0) {
setTimeout(() => {
try {
// 在主图窗格创建 AVG 均价线指标
(chart as any).createIndicator('AVG', true, {
id: 'candle_pane', // 主图窗格
});
console.log('[DEBUG] ✅ 均价线AVG指标添加成功');
} catch (err) {
console.error('[DEBUG] ❌ 均价线添加失败:', err);
}
}, 150); // 延迟 150ms确保数据加载完成后再创建指标
// ✅ 步骤 5: 添加昨收价基准线(灰色虚线)
setTimeout(() => {
try {
const prevClose = klineData[0]?.prev_close;
if (prevClose && prevClose > 0) {
// 创建水平线覆盖层
(chart as any).createOverlay({
name: 'horizontalStraightLine',
id: 'prev_close_line',
points: [{ value: prevClose }],
styles: {
line: {
style: 'dashed',
dashValue: [4, 2],
size: 1,
color: '#888888', // 灰色虚线
},
},
extendData: {
label: `昨收: ${prevClose.toFixed(2)}`,
},
});
console.log('[DEBUG] ✅ 昨收价基准线添加成功:', prevClose);
}
} catch (err) {
console.error('[DEBUG] ❌ 昨收价基准线添加失败:', err);
}
}, 200); // 延迟 200ms确保均价线创建完成后再添加
}
logger.debug(
'useKLineData',
`updateChartData - ${stockCode} (${chartType}) - ${klineData.length}条数据加载成功`
);
} catch (err) {
logger.error('useKLineData', 'updateChartData', err as Error, {
dataCount: klineData.length,
});
}
},
[chart, stockCode, chartType]
);
/**
* 手动更新数据(外部调用)
*/
const updateData = useCallback(
(newData: KLineDataPoint[]) => {
setData(newData);
updateChartData(newData);
logger.debug(
'useKLineData',
`updateData - ${stockCode} (${chartType}) - ${newData.length}条数据手动更新`
);
},
[updateChartData]
);
/**
* 清空数据
*/
const clearData = useCallback(() => {
setData([]);
setRawData([]);
setError(null);
if (chart) {
chart.resetData();
logger.debug('useKLineData', `clearData - chartId: ${(chart as any).id}`);
}
}, [chart]);
// 自动加载数据(当 stockCode/chartType/eventTime 变化时)
useEffect(() => {
if (autoLoad && stockCode && chart) {
loadData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stockCode, chartType, eventTime, autoLoad, chart]);
// 数据变化时更新图表
useEffect(() => {
if (data.length > 0 && chart) {
updateChartData(data);
}
}, [data, chart, updateChartData]);
return {
data,
rawData,
loading,
error,
loadData,
updateData,
clearData,
};
};

View File

@@ -0,0 +1,93 @@
/**
* 自定义均价线指标
*
* 用于分时图显示橙黄色均价线
* 计算公式:累计成交额 / 累计成交量
*/
import type { Indicator, KLineData } from 'klinecharts';
export const avgPriceIndicator: Indicator = {
name: 'AVG',
shortName: 'AVG',
calcParams: [],
shouldOhlc: false, // 不显示 OHLC 信息
shouldFormatBigNumber: false,
precision: 2,
minValue: null,
maxValue: null,
figures: [
{
key: 'avg',
title: '均价: ',
type: 'line',
},
],
/**
* 计算均价
* @param dataList K线数据列表
* @returns 均价数据
*/
calc: (dataList: KLineData[]) => {
let totalAmount = 0; // 累计成交额
let totalVolume = 0; // 累计成交量
return dataList.map((kLineData) => {
const { close = 0, volume = 0 } = kLineData;
totalAmount += close * volume;
totalVolume += volume;
const avgPrice = totalVolume > 0 ? totalAmount / totalVolume : close;
return { avg: avgPrice };
});
},
/**
* 绘制样式配置
*/
styles: {
lines: [
{
color: '#FF9800', // 橙黄色
size: 2,
style: 'solid',
smooth: true,
},
],
},
/**
* Tooltip 格式化(显示均价 + 涨跌幅)
*/
createTooltipDataSource: ({ kLineData, indicator, defaultStyles }: any) => {
if (!indicator?.avg) {
return {
title: { text: '均价', color: defaultStyles.tooltip.text.color },
value: { text: '--', color: '#FF9800' },
};
}
const avgPrice = indicator.avg;
const prevClose = kLineData?.prev_close;
// 计算均价涨跌幅
let changeText = `¥${avgPrice.toFixed(2)}`;
if (prevClose && prevClose > 0) {
const changePercent = ((avgPrice - prevClose) / prevClose * 100).toFixed(2);
const changeValue = (avgPrice - prevClose).toFixed(2);
changeText = `¥${avgPrice.toFixed(2)} (${changeValue}, ${changePercent}%)`;
}
return {
title: { text: '均价', color: defaultStyles.tooltip.text.color },
value: {
text: changeText,
color: '#FF9800',
},
};
},
};

View File

@@ -0,0 +1,126 @@
/**
* KLineChart 图表类型定义
*
* 适配 klinecharts@10.0.0-beta1
* 文档: https://github.com/klinecharts/KLineChart
*/
/**
* K 线数据点(符合 KLineChart 10.0 规范)
*
* 注意: 10.0 版本要求 timestamp 为数字类型(毫秒时间戳)
*/
export interface KLineDataPoint {
/** 时间戳(毫秒) */
timestamp: number;
/** 开盘价 */
open: number;
/** 最高价 */
high: number;
/** 最低价 */
low: number;
/** 收盘价 */
close: number;
/** 成交量 */
volume: number;
/** 成交额(可选) */
turnover?: number;
/** 昨收价(用于百分比计算和基准线)- 分时图专用 */
prev_close?: number;
}
/**
* 后端原始数据格式
*
* 支持多种时间字段格式time/date/timestamp
*/
export interface RawDataPoint {
/** 时间字符串分时图格式HH:mm */
time?: string;
/** 日期字符串日线格式YYYY-MM-DD */
date?: string;
/** 时间戳字符串或数字 */
timestamp?: string | number;
/** 开盘价 */
open: number;
/** 最高价 */
high: number;
/** 最低价 */
low: number;
/** 收盘价 */
close: number;
/** 成交量 */
volume: number;
/** 均价(分时图专用) */
avg_price?: number;
/** 昨收价(用于百分比计算和基准线)- 分时图专用 */
prev_close?: number;
}
/**
* 图表类型枚举
*/
export type ChartType = 'timeline' | 'daily';
/**
* 图表配置接口
*/
export interface ChartConfig {
/** 图表类型 */
type: ChartType;
/** 显示技术指标 */
showIndicators: boolean;
/** 默认技术指标列表 */
defaultIndicators?: string[];
/** 图表高度px */
height?: number;
/** 是否显示网格 */
showGrid?: boolean;
/** 是否显示十字光标 */
showCrosshair?: boolean;
}
/**
* 事件标记接口
*
* 用于在 K 线图上标记重要事件发生时间点
*/
export interface EventMarker {
/** 唯一标识 */
id: string;
/** 时间戳(毫秒) */
timestamp: number;
/** 标签文本 */
label: string;
/** 标记位置 */
position: 'top' | 'middle' | 'bottom';
/** 标记颜色 */
color: string;
/** 图标(可选) */
icon?: string;
/** 是否可拖动(默认 false */
draggable?: boolean;
}
/**
* DataLoader 回调参数KLineChart 10.0 新增)
*/
export interface DataLoaderCallbackParams {
/** K 线数据 */
data: KLineDataPoint[];
/** 是否还有更多数据 */
more: boolean;
}
/**
* DataLoader getBars 参数KLineChart 10.0 新增)
*/
export interface DataLoaderGetBarsParams {
/** 回调函数 */
callback: (data: KLineDataPoint[], options?: { more: boolean }) => void;
/** 范围参数(可选) */
range?: {
from: number;
to: number;
};
}

View File

@@ -0,0 +1,25 @@
/**
* StockChart 类型定义统一导出
*
* 使用方式:
* import type { KLineDataPoint, StockInfo } from '@components/StockChart/types';
*/
// 图表相关类型
export type {
KLineDataPoint,
RawDataPoint,
ChartType,
ChartConfig,
EventMarker,
DataLoaderCallbackParams,
DataLoaderGetBarsParams,
} from './chart.types';
// 股票相关类型
export type {
StockInfo,
ChartDataResponse,
StockQuote,
EventInfo,
} from './stock.types';

View File

@@ -0,0 +1,80 @@
/**
* 股票相关类型定义
*
* 用于股票信息和图表数据的类型声明
*/
import type { RawDataPoint } from './chart.types';
/**
* 股票基础信息
*/
export interface StockInfo {
/** 股票代码600000.SH */
stock_code: string;
/** 股票名称(如:浦发银行) */
stock_name: string;
/** 关联描述(可能是字符串或对象) */
relation_desc?:
| string
| {
/** 数据字段 */
data?: string;
/** 内容字段 */
content?: string;
};
}
/**
* 图表数据 API 响应格式
*/
export interface ChartDataResponse {
/** K 线数据数组 */
data: RawDataPoint[];
/** 交易日期YYYY-MM-DD */
trade_date?: string;
/** 昨收价 */
prev_close?: number;
/** 状态码(可选) */
code?: number;
/** 消息(可选) */
message?: string;
}
/**
* 股票实时行情
*/
export interface StockQuote {
/** 股票代码 */
stock_code: string;
/** 当前价 */
price: number;
/** 涨跌幅(% */
change_percent: number;
/** 涨跌额 */
change_amount: number;
/** 成交量 */
volume: number;
/** 成交额 */
turnover: number;
/** 更新时间 */
update_time: string;
}
/**
* 事件信息(用于事件中心)
*/
export interface EventInfo {
/** 事件 ID */
id: number | string;
/** 事件标题 */
title: string;
/** 事件内容 */
content: string;
/** 事件发生时间ISO 字符串) */
event_time: string;
/** 重要性等级1-5 */
importance?: number;
/** 关联股票列表 */
related_stocks?: StockInfo[];
}

View File

@@ -0,0 +1,295 @@
/**
* 图表通用工具函数
*
* 包含图表初始化、技术指标管理等通用逻辑
*/
import type { Chart } from 'klinecharts';
import { logger } from '@utils/logger';
/**
* 安全地执行图表操作(捕获异常)
*
* @param operation 操作名称
* @param fn 执行函数
* @returns T | null 执行结果或 null
*/
export const safeChartOperation = <T>(
operation: string,
fn: () => T
): T | null => {
try {
return fn();
} catch (error) {
logger.error('chartUtils', operation, error as Error);
return null;
}
};
/**
* 创建技术指标
*
* @param chart KLineChart 实例
* @param indicatorName 指标名称(如 'MA', 'MACD', 'VOL'
* @param params 指标参数(可选)
* @param isStack 是否叠加(主图指标为 true副图为 false
* @returns string | null 指标 ID
*/
export const createIndicator = (
chart: Chart,
indicatorName: string,
params?: number[],
isStack: boolean = false
): string | null => {
return safeChartOperation(`createIndicator:${indicatorName}`, () => {
const indicatorId = chart.createIndicator(
{
name: indicatorName,
...(params && { calcParams: params }),
},
isStack
);
logger.debug('chartUtils', '创建技术指标 (createIndicator)', {
indicatorName,
params,
isStack,
indicatorId,
});
return indicatorId;
});
};
/**
* 移除技术指标
*
* @param chart KLineChart 实例
* @param indicatorId 指标 ID不传则移除所有指标
*/
export const removeIndicator = (chart: Chart, indicatorId?: string): void => {
safeChartOperation('removeIndicator', () => {
chart.removeIndicator(indicatorId);
logger.debug('chartUtils', '移除技术指标 (removeIndicator)', { indicatorId });
});
};
/**
* 批量创建副图指标
*
* @param chart KLineChart 实例
* @param indicators 指标名称数组
* @returns string[] 指标 ID 数组
*/
export const createSubIndicators = (
chart: Chart,
indicators: string[]
): string[] => {
const ids: string[] = [];
indicators.forEach((name) => {
const id = createIndicator(chart, name, undefined, false);
if (id) {
ids.push(id);
}
});
logger.debug('chartUtils', '批量创建副图指标 (createSubIndicators)', {
indicators,
createdIds: ids,
});
return ids;
};
/**
* 设置图表缩放级别
*
* @param chart KLineChart 实例
* @param zoom 缩放级别0.5 - 2.0
*/
export const setChartZoom = (chart: Chart, zoom: number): void => {
safeChartOperation('setChartZoom', () => {
// KLineChart 10.0: 使用 setBarSpace 方法调整 K 线宽度(实现缩放效果)
const baseBarSpace = 8; // 默认 K 线宽度px
const newBarSpace = Math.max(4, Math.min(16, baseBarSpace * zoom));
// 注意KLineChart 10.0 可能没有直接的 zoom API需要通过调整样式实现
chart.setStyles({
candle: {
bar: {
upBorderColor: undefined, // 保持默认
upColor: undefined,
downBorderColor: undefined,
downColor: undefined,
},
// 通过调整蜡烛图宽度实现缩放效果
tooltip: {
showRule: 'always',
},
},
});
logger.debug('chartUtils', '设置图表缩放 (setChartZoom)', {
zoom,
newBarSpace,
});
});
};
/**
* 滚动到指定时间
*
* @param chart KLineChart 实例
* @param timestamp 目标时间戳
*/
export const scrollToTimestamp = (chart: Chart, timestamp: number): void => {
safeChartOperation('scrollToTimestamp', () => {
// KLineChart 10.0: 使用 scrollToTimestamp 方法
chart.scrollToTimestamp(timestamp);
logger.debug('chartUtils', '滚动到指定时间 (scrollToTimestamp)', { timestamp });
});
};
/**
* 调整图表大小(响应式)
*
* @param chart KLineChart 实例
*/
export const resizeChart = (chart: Chart): void => {
safeChartOperation('resizeChart', () => {
chart.resize();
logger.debug('chartUtils', '调整图表大小 (resizeChart)');
});
};
/**
* 获取图表可见数据范围
*
* @param chart KLineChart 实例
* @returns { from: number, to: number } | null 可见范围
*/
export const getVisibleRange = (chart: Chart): { from: number; to: number } | null => {
return safeChartOperation('getVisibleRange', () => {
const data = chart.getDataList();
if (!data || data.length === 0) {
return null;
}
// 简化实现:返回所有数据范围
// 实际项目中可通过 chart 的内部状态获取可见范围
return {
from: 0,
to: data.length - 1,
};
});
};
/**
* 清空图表数据
*
* @param chart KLineChart 实例
*/
export const clearChartData = (chart: Chart): void => {
safeChartOperation('clearChartData', () => {
chart.resetData();
logger.debug('chartUtils', '清空图表数据 (clearChartData)');
});
};
/**
* 截图(导出图表为图片)
*
* @param chart KLineChart 实例
* @param includeOverlay 是否包含 overlay
* @returns string | null Base64 图片数据
*/
export const exportChartImage = (
chart: Chart,
includeOverlay: boolean = true
): string | null => {
return safeChartOperation('exportChartImage', () => {
// KLineChart 10.0: 使用 getConvertPictureUrl 方法
const imageData = chart.getConvertPictureUrl(includeOverlay, 'png', '#ffffff');
logger.debug('chartUtils', '导出图表图片 (exportChartImage)', {
includeOverlay,
hasData: !!imageData,
});
return imageData;
});
};
/**
* 切换十字光标显示
*
* @param chart KLineChart 实例
* @param show 是否显示
*/
export const toggleCrosshair = (chart: Chart, show: boolean): void => {
safeChartOperation('toggleCrosshair', () => {
chart.setStyles({
crosshair: {
show,
},
});
logger.debug('chartUtils', '切换十字光标 (toggleCrosshair)', { show });
});
};
/**
* 切换网格显示
*
* @param chart KLineChart 实例
* @param show 是否显示
*/
export const toggleGrid = (chart: Chart, show: boolean): void => {
safeChartOperation('toggleGrid', () => {
chart.setStyles({
grid: {
show,
},
});
logger.debug('chartUtils', '切换网格 (toggleGrid)', { show });
});
};
/**
* 订阅图表事件
*
* @param chart KLineChart 实例
* @param eventName 事件名称
* @param handler 事件处理函数
*/
export const subscribeChartEvent = (
chart: Chart,
eventName: string,
handler: (...args: any[]) => void
): void => {
safeChartOperation(`subscribeChartEvent:${eventName}`, () => {
chart.subscribeAction(eventName, handler);
logger.debug('chartUtils', '订阅图表事件 (subscribeChartEvent)', { eventName });
});
};
/**
* 取消订阅图表事件
*
* @param chart KLineChart 实例
* @param eventName 事件名称
* @param handler 事件处理函数
*/
export const unsubscribeChartEvent = (
chart: Chart,
eventName: string,
handler: (...args: any[]) => void
): void => {
safeChartOperation(`unsubscribeChartEvent:${eventName}`, () => {
chart.unsubscribeAction(eventName, handler);
logger.debug('chartUtils', '取消订阅图表事件 (unsubscribeChartEvent)', { eventName });
});
};

View File

@@ -0,0 +1,320 @@
/**
* 数据转换适配器
*
* 将后端返回的各种格式数据转换为 KLineChart 10.0 所需的标准格式
*/
import dayjs from 'dayjs';
import type { KLineDataPoint, RawDataPoint, ChartType } from '../types';
import { logger } from '@utils/logger';
/**
* 将后端原始数据转换为 KLineChart 标准格式
*
* @param rawData 后端原始数据数组
* @param chartType 图表类型timeline/daily
* @param eventTime 事件时间(用于日期基准)
* @returns KLineDataPoint[] 标准K线数据
*/
export const convertToKLineData = (
rawData: RawDataPoint[],
chartType: ChartType,
eventTime?: string
): KLineDataPoint[] => {
if (!rawData || !Array.isArray(rawData) || rawData.length === 0) {
logger.warn('dataAdapter', '原始数据为空 (convertToKLineData)', { chartType });
return [];
}
try {
return rawData.map((item, index) => {
const timestamp = parseTimestamp(item, chartType, eventTime, index);
return {
timestamp,
open: Number(item.open) || 0,
high: Number(item.high) || 0,
low: Number(item.low) || 0,
close: Number(item.close) || 0,
volume: Number(item.volume) || 0,
turnover: item.turnover ? Number(item.turnover) : undefined,
prev_close: item.prev_close ? Number(item.prev_close) : undefined, // ✅ 新增:昨收价(用于百分比计算和基准线)
};
});
} catch (error) {
logger.error('dataAdapter', 'convertToKLineData', error as Error, {
chartType,
dataLength: rawData.length,
});
return [];
}
};
/**
* 解析时间戳(兼容多种时间格式)
*
* @param item 原始数据项
* @param chartType 图表类型
* @param eventTime 事件时间
* @param index 数据索引(用于分时图时间推算)
* @returns number 毫秒时间戳
*/
const parseTimestamp = (
item: RawDataPoint,
chartType: ChartType,
eventTime?: string,
index?: number
): number => {
// 优先级1: 使用 timestamp 字段
if (item.timestamp) {
const ts = typeof item.timestamp === 'number' ? item.timestamp : Number(item.timestamp);
// 判断是秒级还是毫秒级时间戳
return ts > 10000000000 ? ts : ts * 1000;
}
// 优先级2: 使用 date 字段日K线
if (item.date) {
return dayjs(item.date).valueOf();
}
// 优先级3: 使用 time 字段(分时图)
if (item.time && eventTime) {
return parseTimelineTimestamp(item.time, eventTime);
}
// 优先级4: 根据 chartType 和 index 推算(兜底逻辑)
if (chartType === 'timeline' && eventTime && typeof index === 'number') {
// 分时图:从事件时间推算(假设 09:30 开盘)
const baseTime = dayjs(eventTime).startOf('day').add(9, 'hour').add(30, 'minute');
return baseTime.add(index, 'minute').valueOf();
}
// 默认返回当前时间(避免图表崩溃)
logger.warn('dataAdapter', '无法解析时间戳,使用当前时间 (parseTimestamp)', { item });
return Date.now();
};
/**
* 解析分时图时间戳
*
* 将 "HH:mm" 格式转换为完整时间戳
*
* @param time 时间字符串(如 "09:30"
* @param eventTime 事件时间YYYY-MM-DD HH:mm:ss
* @returns number 毫秒时间戳
*/
const parseTimelineTimestamp = (time: string, eventTime: string): number => {
try {
const [hours, minutes] = time.split(':').map(Number);
const eventDate = dayjs(eventTime).startOf('day');
return eventDate.hour(hours).minute(minutes).second(0).valueOf();
} catch (error) {
logger.error('dataAdapter', 'parseTimelineTimestamp', error as Error, { time, eventTime });
return dayjs(eventTime).valueOf();
}
};
/**
* 数据验证和清洗
*
* 移除无效数据(价格/成交量异常)
*
* @param data K线数据
* @returns KLineDataPoint[] 清洗后的数据
*/
export const validateAndCleanData = (data: KLineDataPoint[]): KLineDataPoint[] => {
return data.filter((item) => {
// 移除价格为 0 或负数的数据
if (item.open <= 0 || item.high <= 0 || item.low <= 0 || item.close <= 0) {
logger.warn('dataAdapter', '价格异常,已移除 (validateAndCleanData)', { item });
return false;
}
// 移除 high < low 的数据(数据错误)
if (item.high < item.low) {
logger.warn('dataAdapter', '最高价 < 最低价,已移除 (validateAndCleanData)', { item });
return false;
}
// 移除成交量为负数的数据
if (item.volume < 0) {
logger.warn('dataAdapter', '成交量异常,已移除 (validateAndCleanData)', { item });
return false;
}
return true;
});
};
/**
* 数据排序(按时间升序)
*
* @param data K线数据
* @returns KLineDataPoint[] 排序后的数据
*/
export const sortDataByTime = (data: KLineDataPoint[]): KLineDataPoint[] => {
return [...data].sort((a, b) => a.timestamp - b.timestamp);
};
/**
* 数据去重(移除时间戳重复的数据,保留最后一条)
*
* @param data K线数据
* @returns KLineDataPoint[] 去重后的数据
*/
export const deduplicateData = (data: KLineDataPoint[]): KLineDataPoint[] => {
const map = new Map<number, KLineDataPoint>();
data.forEach((item) => {
map.set(item.timestamp, item); // 相同时间戳会覆盖
});
return Array.from(map.values());
};
/**
* 根据事件时间裁剪数据范围前后2周
*
* @param data K线数据
* @param eventTime 事件时间ISO字符串
* @param chartType 图表类型
* @returns KLineDataPoint[] 裁剪后的数据
*/
export const trimDataByEventTime = (
data: KLineDataPoint[],
eventTime: string,
chartType: ChartType
): KLineDataPoint[] => {
if (!eventTime || !data || data.length === 0) {
return data;
}
try {
const eventTimestamp = dayjs(eventTime).valueOf();
// 根据图表类型设置不同的时间范围
let beforeDays: number;
let afterDays: number;
if (chartType === 'timeline') {
// 分时图只显示事件当天前后0天
beforeDays = 0;
afterDays = 0;
} else {
// 日K线显示前后14天2周
beforeDays = 14;
afterDays = 14;
}
const startTime = dayjs(eventTime).subtract(beforeDays, 'day').startOf('day').valueOf();
const endTime = dayjs(eventTime).add(afterDays, 'day').endOf('day').valueOf();
const trimmedData = data.filter((item) => {
return item.timestamp >= startTime && item.timestamp <= endTime;
});
logger.debug('dataAdapter', '数据时间范围裁剪完成 (trimDataByEventTime)', {
originalLength: data.length,
trimmedLength: trimmedData.length,
eventTime,
chartType,
dateRange: `${dayjs(startTime).format('YYYY-MM-DD')} ~ ${dayjs(endTime).format('YYYY-MM-DD')}`,
});
return trimmedData;
} catch (error) {
logger.error('dataAdapter', 'trimDataByEventTime', error as Error, { eventTime });
return data; // 出错时返回原始数据
}
};
/**
* 完整的数据处理流程
*
* 转换 → 验证 → 去重 → 排序 → 时间裁剪(如果有 eventTime
*
* @param rawData 后端原始数据
* @param chartType 图表类型
* @param eventTime 事件时间
* @returns KLineDataPoint[] 处理后的数据
*/
export const processChartData = (
rawData: RawDataPoint[],
chartType: ChartType,
eventTime?: string
): KLineDataPoint[] => {
// 1. 转换数据格式
let data = convertToKLineData(rawData, chartType, eventTime);
// 2. 验证和清洗
data = validateAndCleanData(data);
// 3. 去重
data = deduplicateData(data);
// 4. 排序
data = sortDataByTime(data);
// 5. 根据事件时间裁剪范围(如果提供了 eventTime
if (eventTime) {
data = trimDataByEventTime(data, eventTime, chartType);
}
logger.debug('dataAdapter', '数据处理完成 (processChartData)', {
rawLength: rawData.length,
processedLength: data.length,
chartType,
hasEventTime: !!eventTime,
});
return data;
};
/**
* 获取数据时间范围
*
* @param data K线数据
* @returns { start: number, end: number } 时间范围(毫秒时间戳)
*/
export const getDataTimeRange = (
data: KLineDataPoint[]
): { start: number; end: number } | null => {
if (!data || data.length === 0) {
return null;
}
const timestamps = data.map((item) => item.timestamp);
return {
start: Math.min(...timestamps),
end: Math.max(...timestamps),
};
};
/**
* 查找最接近指定时间的数据点
*
* @param data K线数据
* @param targetTime 目标时间戳
* @returns KLineDataPoint | null 最接近的数据点
*/
export const findClosestDataPoint = (
data: KLineDataPoint[],
targetTime: number
): KLineDataPoint | null => {
if (!data || data.length === 0) {
return null;
}
let closest = data[0];
let minDiff = Math.abs(data[0].timestamp - targetTime);
data.forEach((item) => {
const diff = Math.abs(item.timestamp - targetTime);
if (diff < minDiff) {
minDiff = diff;
closest = item;
}
});
return closest;
};

View File

@@ -0,0 +1,360 @@
/**
* 事件标记工具函数
*
* 用于在 K 线图上创建、管理事件标记Overlay
*/
import dayjs from 'dayjs';
import type { OverlayCreate } from 'klinecharts';
import type { EventMarker, KLineDataPoint } from '../types';
import { EVENT_MARKER_CONFIG } from '../config';
import { findClosestDataPoint } from './dataAdapter';
import { logger } from '@utils/logger';
/**
* 创建事件标记 OverlayKLineChart 10.0 格式)
*
* @param marker 事件标记配置
* @param data K线数据用于定位标记位置
* @returns OverlayCreate | null Overlay 配置对象
*/
export const createEventMarkerOverlay = (
marker: EventMarker,
data: KLineDataPoint[]
): OverlayCreate | null => {
try {
// 查找最接近事件时间的数据点
const closestPoint = findClosestDataPoint(data, marker.timestamp);
if (!closestPoint) {
logger.warn('eventMarkerUtils', '未找到匹配的数据点', {
markerId: marker.id,
timestamp: marker.timestamp,
});
return null;
}
// 根据位置计算 Y 坐标
const yValue = calculateMarkerYPosition(closestPoint, marker.position);
// 创建 Overlay 配置KLineChart 10.0 规范)
const overlay: OverlayCreate = {
name: 'simpleAnnotation', // 使用内置的简单标注类型
id: marker.id,
points: [
{
timestamp: closestPoint.timestamp,
value: yValue,
},
],
styles: {
point: {
color: marker.color,
borderColor: marker.color,
borderSize: 2,
radius: EVENT_MARKER_CONFIG.size.point,
},
text: {
color: EVENT_MARKER_CONFIG.text.color,
size: EVENT_MARKER_CONFIG.text.fontSize,
family: EVENT_MARKER_CONFIG.text.fontFamily,
weight: 'bold',
},
rect: {
style: 'fill',
color: marker.color,
borderRadius: EVENT_MARKER_CONFIG.text.borderRadius,
paddingLeft: EVENT_MARKER_CONFIG.text.padding,
paddingRight: EVENT_MARKER_CONFIG.text.padding,
paddingTop: EVENT_MARKER_CONFIG.text.padding,
paddingBottom: EVENT_MARKER_CONFIG.text.padding,
},
},
// 标记文本内容
extendData: {
label: marker.label,
icon: marker.icon,
},
};
logger.debug('eventMarkerUtils', '创建事件标记', {
markerId: marker.id,
timestamp: closestPoint.timestamp,
label: marker.label,
});
return overlay;
} catch (error) {
logger.error('eventMarkerUtils', 'createEventMarkerOverlay', error as Error, {
markerId: marker.id,
});
return null;
}
};
/**
* 创建事件日K线黄色高亮覆盖层垂直矩形背景
*
* @param eventTime 事件时间ISO字符串
* @param data K线数据
* @returns OverlayCreate | null 高亮覆盖层配置
*/
export const createEventHighlightOverlay = (
eventTime: string,
data: KLineDataPoint[]
): OverlayCreate | null => {
try {
const eventTimestamp = dayjs(eventTime).valueOf();
const closestPoint = findClosestDataPoint(data, eventTimestamp);
if (!closestPoint) {
logger.warn('eventMarkerUtils', '未找到匹配的数据点');
return null;
}
// 创建垂直矩形覆盖层(从图表顶部到底部的黄色半透明背景)
const overlay: OverlayCreate = {
name: 'rect', // 矩形覆盖层
id: `event-highlight-${eventTimestamp}`,
points: [
{
timestamp: closestPoint.timestamp,
value: closestPoint.high * 1.05, // 顶部位置高于最高价5%
},
{
timestamp: closestPoint.timestamp,
value: closestPoint.low * 0.95, // 底部位置低于最低价5%
},
],
styles: {
style: 'fill',
color: 'rgba(255, 193, 7, 0.15)', // 黄色半透明背景15%透明度)
borderColor: '#FFD54F', // 黄色边框
borderSize: 2,
borderStyle: 'solid',
},
};
logger.debug('eventMarkerUtils', '创建事件高亮覆盖层', {
timestamp: closestPoint.timestamp,
eventTime,
});
return overlay;
} catch (error) {
logger.error('eventMarkerUtils', 'createEventHighlightOverlay', error as Error);
return null;
}
};
/**
* 计算标记的 Y 轴位置
*
* @param dataPoint K线数据点
* @param position 标记位置top/middle/bottom
* @returns number Y轴数值
*/
const calculateMarkerYPosition = (
dataPoint: KLineDataPoint,
position: 'top' | 'middle' | 'bottom'
): number => {
switch (position) {
case 'top':
return dataPoint.high * 1.02; // 在最高价上方 2%
case 'bottom':
return dataPoint.low * 0.98; // 在最低价下方 2%
case 'middle':
default:
return (dataPoint.high + dataPoint.low) / 2; // 中间位置
}
};
/**
* 从事件时间创建标记配置
*
* @param eventTime 事件时间字符串ISO 格式)
* @param label 标记标签(可选,默认为"事件发生"
* @param color 标记颜色(可选,使用默认颜色)
* @returns EventMarker 事件标记配置
*/
export const createEventMarkerFromTime = (
eventTime: string,
label: string = '事件发生',
color: string = EVENT_MARKER_CONFIG.defaultColor
): EventMarker => {
const timestamp = dayjs(eventTime).valueOf();
return {
id: `event-${timestamp}`,
timestamp,
label,
position: EVENT_MARKER_CONFIG.defaultPosition,
color,
icon: EVENT_MARKER_CONFIG.defaultIcon,
draggable: false,
};
};
/**
* 批量创建事件标记 Overlays
*
* @param markers 事件标记配置数组
* @param data K线数据
* @returns OverlayCreate[] Overlay 配置数组
*/
export const createEventMarkerOverlays = (
markers: EventMarker[],
data: KLineDataPoint[]
): OverlayCreate[] => {
if (!markers || markers.length === 0) {
return [];
}
const overlays: OverlayCreate[] = [];
markers.forEach((marker) => {
const overlay = createEventMarkerOverlay(marker, data);
if (overlay) {
overlays.push(overlay);
}
});
logger.debug('eventMarkerUtils', '批量创建事件标记', {
totalMarkers: markers.length,
createdOverlays: overlays.length,
});
return overlays;
};
/**
* 移除事件标记
*
* @param chart KLineChart 实例
* @param markerId 标记 ID
*/
export const removeEventMarker = (chart: any, markerId: string): void => {
try {
chart.removeOverlay(markerId);
logger.debug('eventMarkerUtils', '移除事件标记', { markerId });
} catch (error) {
logger.error('eventMarkerUtils', 'removeEventMarker', error as Error, { markerId });
}
};
/**
* 移除所有事件标记
*
* @param chart KLineChart 实例
*/
export const removeAllEventMarkers = (chart: any): void => {
try {
// KLineChart 10.0 API: removeOverlay() 不传参数时移除所有 overlays
chart.removeOverlay();
logger.debug('eventMarkerUtils', '移除所有事件标记');
} catch (error) {
logger.error('eventMarkerUtils', 'removeAllEventMarkers', error as Error);
}
};
/**
* 更新事件标记
*
* @param chart KLineChart 实例
* @param markerId 标记 ID
* @param updates 更新内容(部分字段)
*/
export const updateEventMarker = (
chart: any,
markerId: string,
updates: Partial<EventMarker>
): void => {
try {
// 先移除旧标记
removeEventMarker(chart, markerId);
// 重新创建标记KLineChart 10.0 不支持直接更新 overlay
// 注意:需要在调用方重新创建并添加 overlay
logger.debug('eventMarkerUtils', '更新事件标记', {
markerId,
updates,
});
} catch (error) {
logger.error('eventMarkerUtils', 'updateEventMarker', error as Error, { markerId });
}
};
/**
* 高亮事件标记(改变样式)
*
* @param chart KLineChart 实例
* @param markerId 标记 ID
* @param highlight 是否高亮
*/
export const highlightEventMarker = (
chart: any,
markerId: string,
highlight: boolean
): void => {
try {
// KLineChart 10.0: 通过 overrideOverlay 修改样式
chart.overrideOverlay({
id: markerId,
styles: {
point: {
activeRadius: highlight ? 10 : EVENT_MARKER_CONFIG.size.point,
activeBorderSize: highlight ? 3 : 2,
},
},
});
logger.debug('eventMarkerUtils', '高亮事件标记', {
markerId,
highlight,
});
} catch (error) {
logger.error('eventMarkerUtils', 'highlightEventMarker', error as Error, { markerId });
}
};
/**
* 格式化事件标记标签
*
* @param eventTitle 事件标题
* @param maxLength 最大长度(默认 10
* @returns string 格式化后的标签
*/
export const formatEventMarkerLabel = (eventTitle: string, maxLength: number = 10): string => {
if (!eventTitle) {
return '事件';
}
if (eventTitle.length <= maxLength) {
return eventTitle;
}
return `${eventTitle.substring(0, maxLength)}...`;
};
/**
* 判断事件时间是否在数据范围内
*
* @param eventTime 事件时间戳
* @param data K线数据
* @returns boolean 是否在范围内
*/
export const isEventTimeInDataRange = (
eventTime: number,
data: KLineDataPoint[]
): boolean => {
if (!data || data.length === 0) {
return false;
}
const timestamps = data.map((item) => item.timestamp);
const minTime = Math.min(...timestamps);
const maxTime = Math.max(...timestamps);
return eventTime >= minTime && eventTime <= maxTime;
};

View File

@@ -0,0 +1,48 @@
/**
* StockChart 工具函数统一导出
*
* 使用方式:
* import { processChartData, createEventMarkerOverlay } from '@components/StockChart/utils';
*/
// 数据转换适配器
export {
convertToKLineData,
validateAndCleanData,
sortDataByTime,
deduplicateData,
processChartData,
getDataTimeRange,
findClosestDataPoint,
} from './dataAdapter';
// 事件标记工具
export {
createEventMarkerOverlay,
createEventMarkerFromTime,
createEventMarkerOverlays,
removeEventMarker,
removeAllEventMarkers,
updateEventMarker,
highlightEventMarker,
formatEventMarkerLabel,
isEventTimeInDataRange,
} from './eventMarkerUtils';
// 图表通用工具
export {
safeChartOperation,
createIndicator,
removeIndicator,
createSubIndicators,
setChartZoom,
scrollToTimestamp,
resizeChart,
getVisibleRange,
clearChartData,
exportChartImage,
toggleCrosshair,
toggleGrid,
subscribeChartEvent,
unsubscribeChartEvent,
} from './chartUtils';

View File

@@ -0,0 +1,121 @@
/**
* 关联描述组件
*
* 用于显示股票与事件的关联描述信息
* 固定标题为"关联描述:"
* 自动处理多种数据格式(字符串、对象数组)
*
* @example
* ```tsx
* // 基础使用 - 传入原始 relation_desc 数据
* <RelationDescription relationDesc={stock.relation_desc} />
*
* // 自定义样式
* <RelationDescription
* relationDesc={stock.relation_desc}
* fontSize="md"
* titleColor="blue.700"
* />
* ```
*/
import React, { useMemo } from 'react';
import { Box, Text, BoxProps } from '@chakra-ui/react';
/**
* 关联描述数据类型
* - 字符串格式:直接的描述文本
* - 对象格式:包含多个句子的数组
*/
export type RelationDescType =
| string
| {
data: Array<{
query_part?: string;
sentences?: string;
}>;
}
| null
| undefined;
export interface RelationDescriptionProps {
/** 原始关联描述数据(支持字符串或对象格式) */
relationDesc: RelationDescType;
/** 字体大小,默认 'sm' */
fontSize?: string;
/** 标题颜色,默认 'gray.700' */
titleColor?: string;
/** 文本颜色,默认 'gray.600' */
textColor?: string;
/** 行高,默认 '1.7' */
lineHeight?: string;
/** 容器额外属性 */
containerProps?: BoxProps;
}
export const RelationDescription: React.FC<RelationDescriptionProps> = ({
relationDesc,
fontSize = 'sm',
titleColor = 'gray.700',
textColor = 'gray.600',
lineHeight = '1.7',
containerProps = {}
}) => {
// 处理关联描述(兼容对象和字符串格式)
const processedDesc = useMemo(() => {
if (!relationDesc) return null;
// 字符串格式:直接返回
if (typeof relationDesc === 'string') {
return relationDesc;
}
// 对象格式:提取并拼接文本
if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) {
return (
relationDesc.data
.map((item) => item.query_part || item.sentences || '')
.filter((s) => s)
.join('') || null
);
}
return null;
}, [relationDesc]);
// 如果没有有效的描述内容,不渲染组件
if (!processedDesc) {
return null;
}
return (
<Box
p={4}
borderTop="1px solid"
borderTopColor="gray.200"
{...containerProps}
>
<Text
fontSize={fontSize}
fontWeight="bold"
mb={2}
color={titleColor}
>
:
</Text>
<Text
fontSize={fontSize}
color={textColor}
lineHeight={lineHeight}
whiteSpace="pre-wrap"
>
{processedDesc}
</Text>
</Box>
);
};

View File

@@ -0,0 +1,6 @@
/**
* StockRelation 组件导出入口
*/
export { RelationDescription } from './RelationDescription';
export type { RelationDescriptionProps, RelationDescType } from './RelationDescription';

View File

@@ -0,0 +1,306 @@
/**
* 性能指标阈值配置
* 基于 Google Web Vitals 标准和项目实际情况
*
* 评级标准:
* - good: 绿色,性能优秀
* - needs-improvement: 黄色,需要改进
* - poor: 红色,性能较差
*
* @see https://web.dev/defining-core-web-vitals-thresholds/
*/
// ============================================================
// Web Vitals 官方阈值Google 标准)
// ============================================================
/**
* Largest Contentful Paint (LCP) - 最大内容绘制
* 衡量加载性能,理想情况下应在 2.5 秒内完成
*/
export const LCP_THRESHOLDS = {
good: 2500, // < 2.5s 为优秀
needsImprovement: 4000, // 2.5s - 4s 需要改进
// > 4s 为较差
};
/**
* First Contentful Paint (FCP) - 首次内容绘制
* 衡量首次渲染任何内容的速度
*/
export const FCP_THRESHOLDS = {
good: 1800, // < 1.8s 为优秀
needsImprovement: 3000, // 1.8s - 3s 需要改进
// > 3s 为较差
};
/**
* Cumulative Layout Shift (CLS) - 累积布局偏移
* 衡量视觉稳定性(无单位,分数值)
*/
export const CLS_THRESHOLDS = {
good: 0.1, // < 0.1 为优秀
needsImprovement: 0.25, // 0.1 - 0.25 需要改进
// > 0.25 为较差
};
/**
* First Input Delay (FID) - 首次输入延迟
* 衡量交互性能
*/
export const FID_THRESHOLDS = {
good: 100, // < 100ms 为优秀
needsImprovement: 300, // 100ms - 300ms 需要改进
// > 300ms 为较差
};
/**
* Time to First Byte (TTFB) - 首字节时间
* 衡量服务器响应速度
*/
export const TTFB_THRESHOLDS = {
good: 800, // < 0.8s 为优秀
needsImprovement: 1800, // 0.8s - 1.8s 需要改进
// > 1.8s 为较差
};
// ============================================================
// 自定义指标阈值(项目特定)
// ============================================================
/**
* Time to Interactive (TTI) - 首屏可交互时间
* 自定义指标:从页面加载到用户可以交互的时间
*/
export const TTI_THRESHOLDS = {
good: 3500, // < 3.5s 为优秀
needsImprovement: 7300, // 3.5s - 7.3s 需要改进
// > 7.3s 为较差
};
/**
* 骨架屏展示时长阈值
*/
export const SKELETON_DURATION_THRESHOLDS = {
good: 300, // < 0.3s 为优秀(骨架屏展示时间短)
needsImprovement: 1000, // 0.3s - 1s 需要改进
// > 1s 为较差(骨架屏展示太久)
};
/**
* API 响应时间阈值
*/
export const API_RESPONSE_TIME_THRESHOLDS = {
good: 500, // < 500ms 为优秀
needsImprovement: 1500, // 500ms - 1.5s 需要改进
// > 1.5s 为较差
};
/**
* 资源加载时间阈值
*/
export const RESOURCE_LOAD_TIME_THRESHOLDS = {
good: 2000, // < 2s 为优秀
needsImprovement: 5000, // 2s - 5s 需要改进
// > 5s 为较差
};
/**
* Bundle 大小阈值KB
*/
export const BUNDLE_SIZE_THRESHOLDS = {
js: {
good: 500, // < 500KB 为优秀
needsImprovement: 1000, // 500KB - 1MB 需要改进
// > 1MB 为较差
},
css: {
good: 100, // < 100KB 为优秀
needsImprovement: 200, // 100KB - 200KB 需要改进
// > 200KB 为较差
},
image: {
good: 1500, // < 1.5MB 为优秀
needsImprovement: 3000, // 1.5MB - 3MB 需要改进
// > 3MB 为较差
},
};
/**
* 缓存命中率阈值(百分比)
*/
export const CACHE_HIT_RATE_THRESHOLDS = {
good: 80, // > 80% 为优秀
needsImprovement: 50, // 50% - 80% 需要改进
// < 50% 为较差
};
// ============================================================
// 综合阈值配置对象
// ============================================================
/**
* 所有性能指标的阈值配置(用于类型化访问)
*/
export const PERFORMANCE_THRESHOLDS = {
LCP: LCP_THRESHOLDS,
FCP: FCP_THRESHOLDS,
CLS: CLS_THRESHOLDS,
FID: FID_THRESHOLDS,
TTFB: TTFB_THRESHOLDS,
TTI: TTI_THRESHOLDS,
SKELETON_DURATION: SKELETON_DURATION_THRESHOLDS,
API_RESPONSE_TIME: API_RESPONSE_TIME_THRESHOLDS,
RESOURCE_LOAD_TIME: RESOURCE_LOAD_TIME_THRESHOLDS,
BUNDLE_SIZE: BUNDLE_SIZE_THRESHOLDS,
CACHE_HIT_RATE: CACHE_HIT_RATE_THRESHOLDS,
};
// ============================================================
// 工具函数
// ============================================================
/**
* 根据指标值和阈值计算评级
* @param {number} value - 指标值
* @param {Object} thresholds - 阈值配置对象 { good, needsImprovement }
* @param {boolean} reverse - 是否反向评级(值越大越好,如缓存命中率)
* @returns {'good' | 'needs-improvement' | 'poor'}
*/
export const calculateRating = (value, thresholds, reverse = false) => {
if (!thresholds || typeof value !== 'number') {
return 'poor';
}
const { good, needsImprovement } = thresholds;
if (reverse) {
// 反向评级:值越大越好(如缓存命中率)
if (value >= good) return 'good';
if (value >= needsImprovement) return 'needs-improvement';
return 'poor';
} else {
// 正常评级:值越小越好(如加载时间)
if (value <= good) return 'good';
if (value <= needsImprovement) return 'needs-improvement';
return 'poor';
}
};
/**
* 获取评级对应的颜色Chakra UI 颜色方案)
* @param {'good' | 'needs-improvement' | 'poor'} rating
* @returns {string} Chakra UI 颜色名称
*/
export const getRatingColor = (rating) => {
switch (rating) {
case 'good':
return 'green';
case 'needs-improvement':
return 'yellow';
case 'poor':
return 'red';
default:
return 'gray';
}
};
/**
* 获取评级对应的控制台颜色代码
* @param {'good' | 'needs-improvement' | 'poor'} rating
* @returns {string} ANSI 颜色代码
*/
export const getRatingConsoleColor = (rating) => {
switch (rating) {
case 'good':
return '\x1b[32m'; // 绿色
case 'needs-improvement':
return '\x1b[33m'; // 黄色
case 'poor':
return '\x1b[31m'; // 红色
default:
return '\x1b[0m'; // 重置
}
};
/**
* 获取评级对应的图标
* @param {'good' | 'needs-improvement' | 'poor'} rating
* @returns {string} Emoji 图标
*/
export const getRatingIcon = (rating) => {
switch (rating) {
case 'good':
return '✅';
case 'needs-improvement':
return '⚠️';
case 'poor':
return '❌';
default:
return '❓';
}
};
/**
* 格式化指标值(添加单位)
* @param {string} metricName - 指标名称
* @param {number} value - 指标值
* @returns {string} 格式化后的字符串
*/
export const formatMetricValue = (metricName, value) => {
if (typeof value !== 'number' || isNaN(value)) {
return 'N/A';
}
switch (metricName) {
case 'LCP':
case 'FCP':
case 'FID':
case 'TTFB':
case 'TTI':
case 'SKELETON_DURATION':
case 'API_RESPONSE_TIME':
case 'RESOURCE_LOAD_TIME':
// 时间类指标:转换为秒或毫秒
return value >= 1000
? `${(value / 1000).toFixed(2)}s`
: `${Math.round(value)}ms`;
case 'CLS':
// CLS 是无单位的分数
return value.toFixed(3);
case 'CACHE_HIT_RATE':
// 百分比
return `${value.toFixed(1)}%`;
default:
// 默认保留两位小数
return value.toFixed(2);
}
};
/**
* 批量计算所有 Web Vitals 指标的评级
* @param {Object} metrics - 指标对象 { LCP: value, FCP: value, ... }
* @returns {Object} 评级对象 { LCP: 'good', FCP: 'needs-improvement', ... }
*/
export const calculateAllRatings = (metrics) => {
const ratings = {};
Object.entries(metrics).forEach(([metricName, value]) => {
const thresholds = PERFORMANCE_THRESHOLDS[metricName];
if (thresholds) {
const isReverse = metricName === 'CACHE_HIT_RATE'; // 缓存命中率是反向评级
ratings[metricName] = calculateRating(value, thresholds, isReverse);
}
});
return ratings;
};
// ============================================================
// 默认导出
// ============================================================
export default PERFORMANCE_THRESHOLDS;

View File

@@ -11,7 +11,6 @@ import {
selectRedirectUrl
} from '../store/slices/authModalSlice';
import { useAuth } from '../contexts/AuthContext';
import { logger } from '../utils/logger';
/**
* 认证弹窗自定义 Hook
@@ -49,27 +48,19 @@ export const useAuthModal = () => {
const openAuthModal = useCallback((url = null, callback = null) => {
onSuccessCallbackRef.current = callback;
dispatch(openModal({ redirectUrl: url }));
logger.debug('useAuthModal', '打开认证弹窗', {
redirectUrl: url || '无',
hasCallback: !!callback
});
}, [dispatch]);
/**
* 关闭认证弹窗
* 如果用户未登录,跳转到首页
* 如果用户未登录且不在首页,跳转到首页
*/
const closeAuthModal = useCallback(() => {
dispatch(closeModal());
onSuccessCallbackRef.current = null;
// ⭐ 如果用户关闭弹窗时仍未登录,跳转到首页
if (!isAuthenticated) {
// ⭐ 如果用户关闭弹窗时仍未登录,且当前不在首页,才跳转到首页
if (!isAuthenticated && window.location.pathname !== '/home') {
navigate('/home');
logger.debug('useAuthModal', '未登录关闭弹窗,跳转到首页');
} else {
logger.debug('useAuthModal', '关闭认证弹窗');
}
}, [dispatch, isAuthenticated, navigate]);
@@ -82,14 +73,8 @@ export const useAuthModal = () => {
if (onSuccessCallbackRef.current) {
try {
onSuccessCallbackRef.current(user);
logger.debug('useAuthModal', '执行成功回调', {
userId: user?.id
});
} catch (error) {
logger.error('useAuthModal', 'handleLoginSuccess 回调执行失败', error, {
userId: user?.id,
hasCallback: !!onSuccessCallbackRef.current
});
console.error('useAuthModal: handleLoginSuccess 回调执行失败', error);
}
}
@@ -97,10 +82,6 @@ export const useAuthModal = () => {
// 移除了原有的 redirectUrl 跳转逻辑
dispatch(closeModal());
onSuccessCallbackRef.current = null;
logger.debug('useAuthModal', '登录成功,关闭弹窗', {
userId: user?.id
});
}, [dispatch]);
return {

View File

@@ -0,0 +1,291 @@
/**
* 首屏性能指标收集 Hook
* 整合 Web Vitals、资源加载、API 请求等指标
*
* 使用示例:
* ```tsx
* const { metrics, isLoading, remeasure, exportMetrics } = useFirstScreenMetrics({
* pageType: 'home',
* enableConsoleLog: process.env.NODE_ENV === 'development'
* });
* ```
*
* @module hooks/useFirstScreenMetrics
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { initWebVitalsTracking, getCachedMetrics } from '@utils/performance/webVitals';
import { collectResourceStats, collectApiStats } from '@utils/performance/resourceMonitor';
import posthog from 'posthog-js';
import type {
FirstScreenMetrics,
UseFirstScreenMetricsOptions,
UseFirstScreenMetricsResult,
FirstScreenInteractiveEventProperties,
} from '@/types/metrics';
// ============================================================
// Hook 实现
// ============================================================
/**
* 首屏性能指标收集 Hook
*/
export const useFirstScreenMetrics = (
options: UseFirstScreenMetricsOptions
): UseFirstScreenMetricsResult => {
const {
pageType,
enableConsoleLog = process.env.NODE_ENV === 'development',
trackToPostHog = process.env.NODE_ENV === 'production',
customProperties = {},
} = options;
const [isLoading, setIsLoading] = useState(true);
const [metrics, setMetrics] = useState<FirstScreenMetrics | null>(null);
// 使用 ref 记录页面加载开始时间
const pageLoadStartRef = useRef<number>(performance.now());
const skeletonStartRef = useRef<number>(performance.now());
const hasInitializedRef = useRef(false);
/**
* 收集所有首屏指标
*/
const collectAllMetrics = useCallback((): FirstScreenMetrics => {
try {
// 1. 初始化 Web Vitals 监控
initWebVitalsTracking({
enableConsoleLog,
trackToPostHog: false, // Web Vitals 自己会上报,这里不重复
pageType,
customProperties,
});
// 2. 获取 Web Vitals 指标(延迟获取,等待 LCP/FCP 等指标完成)
const webVitalsCache = getCachedMetrics();
const webVitals = Object.fromEntries(webVitalsCache.entries());
// 3. 收集资源加载统计
const resourceStats = collectResourceStats({
enableConsoleLog,
trackToPostHog: false, // 避免重复上报
pageType,
customProperties,
});
// 4. 收集 API 请求统计
const apiStats = collectApiStats({
enableConsoleLog,
trackToPostHog: false,
pageType,
customProperties,
});
// 5. 计算首屏可交互时间TTI
const now = performance.now();
const timeToInteractive = now - pageLoadStartRef.current;
// 6. 计算骨架屏展示时长
const skeletonDisplayDuration = now - skeletonStartRef.current;
const firstScreenMetrics: FirstScreenMetrics = {
webVitals,
resourceStats,
apiStats,
timeToInteractive,
skeletonDisplayDuration,
measuredAt: Date.now(),
};
return firstScreenMetrics;
} catch (error) {
console.error('Failed to collect first screen metrics:', error);
throw error;
}
}, [pageType, enableConsoleLog, trackToPostHog, customProperties]);
/**
* 上报首屏可交互事件到 PostHog
*/
const trackFirstScreenInteractive = useCallback(
(metrics: FirstScreenMetrics) => {
if (!trackToPostHog || process.env.NODE_ENV !== 'production') {
return;
}
try {
const eventProperties: FirstScreenInteractiveEventProperties = {
tti_seconds: metrics.timeToInteractive / 1000,
skeleton_duration_seconds: metrics.skeletonDisplayDuration / 1000,
api_request_count: metrics.apiStats.totalRequests,
api_avg_response_time_ms: metrics.apiStats.avgResponseTime,
page_type: pageType,
measured_at: metrics.measuredAt,
...customProperties,
};
posthog.capture('First Screen Interactive', eventProperties);
if (enableConsoleLog) {
console.log('📊 Tracked First Screen Interactive to PostHog', eventProperties);
}
} catch (error) {
console.error('Failed to track first screen interactive:', error);
}
},
[pageType, trackToPostHog, enableConsoleLog, customProperties]
);
/**
* 手动触发重新测量
*/
const remeasure = useCallback(() => {
setIsLoading(true);
// 重置计时器
pageLoadStartRef.current = performance.now();
skeletonStartRef.current = performance.now();
// 延迟收集指标(等待 Web Vitals 完成)
setTimeout(() => {
try {
const newMetrics = collectAllMetrics();
setMetrics(newMetrics);
trackFirstScreenInteractive(newMetrics);
if (enableConsoleLog) {
console.group('🎯 First Screen Metrics (Re-measured)');
console.log('TTI:', `${(newMetrics.timeToInteractive / 1000).toFixed(2)}s`);
console.log('Skeleton Duration:', `${(newMetrics.skeletonDisplayDuration / 1000).toFixed(2)}s`);
console.log('API Requests:', newMetrics.apiStats.totalRequests);
console.groupEnd();
}
} catch (error) {
console.error('Failed to remeasure metrics:', error);
} finally {
setIsLoading(false);
}
}, 1000); // 延迟 1 秒收集
}, [collectAllMetrics, trackFirstScreenInteractive, enableConsoleLog]);
/**
* 导出指标为 JSON
*/
const exportMetrics = useCallback((): string => {
if (!metrics) {
return JSON.stringify({ error: 'No metrics available' }, null, 2);
}
return JSON.stringify(metrics, null, 2);
}, [metrics]);
/**
* 初始化:在组件挂载时自动收集指标
*/
useEffect(() => {
// 防止重复初始化
if (hasInitializedRef.current) {
return;
}
hasInitializedRef.current = true;
if (enableConsoleLog) {
console.log('🚀 useFirstScreenMetrics initialized', { pageType });
}
// 延迟收集指标,等待页面渲染完成和 Web Vitals 指标就绪
const timeoutId = setTimeout(() => {
try {
const firstScreenMetrics = collectAllMetrics();
setMetrics(firstScreenMetrics);
trackFirstScreenInteractive(firstScreenMetrics);
if (enableConsoleLog) {
console.group('🎯 First Screen Metrics');
console.log('━'.repeat(50));
console.log(`✅ TTI: ${(firstScreenMetrics.timeToInteractive / 1000).toFixed(2)}s`);
console.log(`✅ Skeleton Duration: ${(firstScreenMetrics.skeletonDisplayDuration / 1000).toFixed(2)}s`);
console.log(`✅ API Requests: ${firstScreenMetrics.apiStats.totalRequests}`);
console.log(`✅ API Avg Response: ${firstScreenMetrics.apiStats.avgResponseTime.toFixed(0)}ms`);
console.log('━'.repeat(50));
console.groupEnd();
}
} catch (error) {
console.error('Failed to collect initial metrics:', error);
} finally {
setIsLoading(false);
}
}, 2000); // 延迟 2 秒收集(确保 LCP/FCP 等指标已触发)
// Cleanup
return () => {
clearTimeout(timeoutId);
};
}, []); // 空依赖数组,只在挂载时执行一次
// ============================================================
// 返回值
// ============================================================
return {
isLoading,
metrics,
remeasure,
exportMetrics,
};
};
// ============================================================
// 辅助 Hook标记骨架屏结束
// ============================================================
/**
* 标记骨架屏结束的 Hook
* 用于在骨架屏消失时记录时间点
*
* 使用示例:
* ```tsx
* const { markSkeletonEnd } = useSkeletonTiming();
*
* useEffect(() => {
* if (!loading) {
* markSkeletonEnd();
* }
* }, [loading, markSkeletonEnd]);
* ```
*/
export const useSkeletonTiming = () => {
const skeletonStartRef = useRef<number>(performance.now());
const skeletonEndRef = useRef<number | null>(null);
const markSkeletonEnd = useCallback(() => {
if (!skeletonEndRef.current) {
skeletonEndRef.current = performance.now();
const duration = skeletonEndRef.current - skeletonStartRef.current;
if (process.env.NODE_ENV === 'development') {
console.log(`⏱️ Skeleton Display Duration: ${(duration / 1000).toFixed(2)}s`);
}
}
}, []);
const getSkeletonDuration = useCallback((): number | null => {
if (skeletonEndRef.current) {
return skeletonEndRef.current - skeletonStartRef.current;
}
return null;
}, []);
return {
markSkeletonEnd,
getSkeletonDuration,
};
};
// ============================================================
// 默认导出
// ============================================================
export default useFirstScreenMetrics;

View File

@@ -2,9 +2,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
// 导入 Tailwind CSS 和 Brainwave 样式
// 导入 Brainwave 样式(空文件,保留以避免错误)
import './styles/brainwave.css';
import './styles/brainwave-colors.css';
// 导入 Select 下拉框颜色修复样式
import './styles/select-fix.css';
// 导入 Bytedesk 客服系统 z-index 覆盖样式(必须在所有样式之后导入)
import './styles/bytedesk-override.css';

View File

@@ -30,12 +30,12 @@ const MemoizedAppFooter = memo(AppFooter);
*/
export default function MainLayout() {
return (
<Box minH="100vh" display="flex" flexDirection="column">
<Box flex="1" h="100vh" w="100%" position="relative" display="flex" flexDirection="column">
{/* 导航栏 - 在所有页面间共享memo 后不会在路由切换时重新渲染 */}
<MemoizedHomeNavbar />
{/* 页面内容区域 - flex: 1 占据剩余空间,包含错误边界、懒加载 */}
<Box flex="1" w="100%" position="relative" overflow="hidden">
<Box flex="1" pt="72px">
<ErrorBoundary>
<Suspense fallback={<PageLoader message="页面加载中..." />}>
<Outlet />
@@ -47,11 +47,11 @@ export default function MainLayout() {
<MemoizedAppFooter />
{/* 返回顶部按钮 - 滚动超过阈值时显示 */}
<BackToTopButton
{/* <BackToTopButton
scrollThreshold={BACK_TO_TOP_CONFIG.scrollThreshold}
position={BACK_TO_TOP_CONFIG.position}
zIndex={BACK_TO_TOP_CONFIG.zIndex}
/>
/> */}
</Box>
);
}

View File

@@ -225,9 +225,19 @@ export const SPECIAL_EVENTS = {
API_ERROR: 'API Error',
NOT_FOUND_404: '404 Not Found',
// Performance
// Performance - Web Vitals
PAGE_LOAD_TIME: 'Page Load Time',
API_RESPONSE_TIME: 'API Response Time',
WEB_VITALS_LCP: 'Web Vitals - LCP', // Largest Contentful Paint
WEB_VITALS_FCP: 'Web Vitals - FCP', // First Contentful Paint
WEB_VITALS_CLS: 'Web Vitals - CLS', // Cumulative Layout Shift
WEB_VITALS_FID: 'Web Vitals - FID', // First Input Delay
WEB_VITALS_TTFB: 'Web Vitals - TTFB', // Time to First Byte
// Performance - First Screen
FIRST_SCREEN_INTERACTIVE: 'First Screen Interactive', // 首屏可交互时间
RESOURCE_LOAD_COMPLETE: 'Resource Load Complete', // 资源加载完成
SKELETON_DISPLAYED: 'Skeleton Displayed', // 骨架屏展示
// Scroll depth
SCROLL_DEPTH_25: 'Scroll Depth 25%',

View File

@@ -78,10 +78,16 @@ function generateTimeRange(data, startTime, endTime, basePrice, session) {
const volume = Math.floor(Math.random() * 500000000 + 100000000);
// ✅ 修复:为分时图添加完整的 OHLC 字段
const closePrice = parseFloat(price.toFixed(2));
data.push({
time: formatTime(current),
price: parseFloat(price.toFixed(2)),
close: parseFloat(price.toFixed(2)),
timestamp: current.getTime(), // ✅ 新增:毫秒时间戳
open: parseFloat((price * 0.9999).toFixed(2)), // ✅ 新增:开盘价(略低于收盘)
high: parseFloat((price * 1.0002).toFixed(2)), // ✅ 新增:最高价(略高于收盘)
low: parseFloat((price * 0.9997).toFixed(2)), // ✅ 新增:最低价(略低于收盘)
close: closePrice, // ✅ 保留:收盘价
price: closePrice, // ✅ 保留:兼容字段(供 MiniTimelineChart 使用)
volume: volume,
prev_close: basePrice
});

233
src/mocks/handlers/agent.js Normal file
View File

@@ -0,0 +1,233 @@
// src/mocks/handlers/agent.js
// Agent Chat API Mock Handlers
import { http, HttpResponse, delay } from 'msw';
// 模拟会话数据
const mockSessions = [
{
session_id: 'session-001',
title: '贵州茅台投资分析',
created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
message_count: 15,
},
{
session_id: 'session-002',
title: '新能源板块研究',
created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
timestamp: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
message_count: 8,
},
{
session_id: 'session-003',
title: '半导体行业分析',
created_at: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
timestamp: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
message_count: 12,
},
];
// 模拟历史消息数据
const mockHistory = {
'session-001': [
{
message_type: 'user',
message: '分析一下贵州茅台的投资价值',
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
plan: null,
steps: null,
},
{
message_type: 'assistant',
message:
'# 贵州茅台投资价值分析\n\n根据最新数据贵州茅台600519.SH具有以下投资亮点\n\n## 基本面分析\n- **营收增长**2024年Q3营收同比增长12.5%\n- **净利润率**保持在50%以上的高水平\n- **ROE**连续10年超过20%\n\n## 估值分析\n- **PETTM**35.6倍,略高于历史中位数\n- **PB**10.2倍,处于合理区间\n\n## 投资建议\n建议关注价格回调机会长期配置价值显著。',
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 + 5000).toISOString(),
plan: JSON.stringify({
goal: '分析贵州茅台的投资价值',
steps: [
{ step: 1, action: '获取贵州茅台最新股价和财务数据', reasoning: '需要基础数据支持分析' },
{ step: 2, action: '分析公司基本面和盈利能力', reasoning: '评估公司质量' },
{ step: 3, action: '对比行业估值水平', reasoning: '判断估值合理性' },
{ step: 4, action: '给出投资建议', reasoning: '综合判断投资价值' },
],
}),
steps: JSON.stringify([
{
tool: 'get_stock_info',
status: 'success',
result: '获取到贵州茅台最新数据股价1850元市值2.3万亿',
},
{
tool: 'analyze_financials',
status: 'success',
result: '财务分析完成:营收增长稳健,利润率行业领先',
},
]),
},
],
};
// 生成模拟的 Agent 响应
function generateAgentResponse(message, sessionId) {
const responses = {
包含: {
贵州茅台: {
plan: {
goal: '分析贵州茅台相关信息',
steps: [
{ step: 1, action: '搜索贵州茅台最新新闻', reasoning: '了解最新动态' },
{ step: 2, action: '获取股票实时行情', reasoning: '查看当前价格走势' },
{ step: 3, action: '分析财务数据', reasoning: '评估基本面' },
{ step: 4, action: '生成投资建议', reasoning: '综合判断' },
],
},
step_results: [
{
tool: 'search_news',
status: 'success',
result: '找到5条相关新闻茅台Q3业绩超预期...',
},
{
tool: 'get_stock_quote',
status: 'success',
result: '当前价格1850元涨幅+2.3%',
},
{
tool: 'analyze_financials',
status: 'success',
result: 'ROE: 25.6%, 净利润率: 52.3%',
},
],
final_summary:
'# 贵州茅台分析报告\n\n## 最新动态\n茅台Q3业绩超预期营收增长稳健。\n\n## 行情分析\n当前价格1850元今日上涨2.3%,成交量活跃。\n\n## 财务表现\n- ROE: 25.6%(行业领先)\n- 净利润率: 52.3%(极高水平)\n- 营收增长: 12.5% YoY\n\n## 投资建议\n**推荐关注**:基本面优秀,估值合理,建议逢低布局。',
},
新能源: {
plan: {
goal: '分析新能源行业',
steps: [
{ step: 1, action: '搜索新能源行业新闻', reasoning: '了解行业动态' },
{ step: 2, action: '获取新能源概念股', reasoning: '找到相关标的' },
{ step: 3, action: '分析行业趋势', reasoning: '判断投资机会' },
],
},
step_results: [
{
tool: 'search_news',
status: 'success',
result: '新能源政策利好频出,行业景气度提升',
},
{
tool: 'get_concept_stocks',
status: 'success',
result: '新能源板块共182只个股今日平均涨幅3.2%',
},
],
final_summary:
'# 新能源行业分析\n\n## 行业动态\n政策利好频出行业景气度持续提升。\n\n## 板块表现\n新能源板块今日强势上涨平均涨幅3.2%。\n\n## 投资机会\n建议关注龙头企业和细分赛道领导者。',
},
},
默认: {
plan: {
goal: '回答用户问题',
steps: [
{ step: 1, action: '理解用户意图', reasoning: '准确把握需求' },
{ step: 2, action: '搜索相关信息', reasoning: '获取数据支持' },
{ step: 3, action: '生成回复', reasoning: '提供专业建议' },
],
},
step_results: [
{
tool: 'search_related_info',
status: 'success',
result: '已找到相关信息',
},
],
final_summary: `我已经收到您的问题:"${message}"\n\n作为您的 AI 投研助手,我可以帮您:\n- 📊 分析股票基本面和技术面\n- 🔥 追踪市场热点和板块动态\n- 📈 研究行业趋势和投资机会\n- 📰 汇总最新财经新闻和研报\n\n请告诉我您想了解哪方面的信息?`,
},
};
// 根据关键词匹配响应
for (const keyword in responses.包含) {
if (message.includes(keyword)) {
return responses.包含[keyword];
}
}
return responses.默认;
}
// Agent Chat API Handlers
export const agentHandlers = [
// POST /mcp/agent/chat - 发送消息
http.post('/mcp/agent/chat', async ({ request }) => {
await delay(800); // 模拟网络延迟
const body = await request.json();
const { message, session_id, user_id, subscription_type } = body;
// 模拟权限检查(仅 max 用户可用)
if (subscription_type !== 'max') {
return HttpResponse.json(
{
success: false,
error: '很抱歉,「价小前投研」功能仅对 Max 订阅用户开放。请升级您的订阅以使用此功能。',
},
{ status: 403 }
);
}
// 生成或使用现有 session_id
const responseSessionId = session_id || `session-${Date.now()}`;
// 根据消息内容生成响应
const response = generateAgentResponse(message, responseSessionId);
return HttpResponse.json({
success: true,
message: '处理成功',
session_id: responseSessionId,
plan: response.plan,
steps: response.step_results,
final_answer: response.final_summary,
metadata: {
model: body.model || 'kimi-k2-thinking',
timestamp: new Date().toISOString(),
},
});
}),
// GET /mcp/agent/sessions - 获取会话列表
http.get('/mcp/agent/sessions', async ({ request }) => {
await delay(300);
const url = new URL(request.url);
const userId = url.searchParams.get('user_id');
const limit = parseInt(url.searchParams.get('limit') || '50');
// 返回模拟的会话列表
const sessions = mockSessions.slice(0, limit);
return HttpResponse.json({
success: true,
data: sessions,
count: sessions.length,
});
}),
// GET /mcp/agent/history/:session_id - 获取会话历史
http.get('/mcp/agent/history/:session_id', async ({ params }) => {
await delay(400);
const { session_id } = params;
// 返回模拟的历史消息
const history = mockHistory[session_id] || [];
return HttpResponse.json({
success: true,
data: history,
count: history.length,
});
}),
];

View File

@@ -15,6 +15,7 @@ import { financialHandlers } from './financial';
import { limitAnalyseHandlers } from './limitAnalyse';
import { posthogHandlers } from './posthog';
import { externalHandlers } from './external';
import { agentHandlers } from './agent';
// 可以在这里添加更多的 handlers
// import { userHandlers } from './user';
@@ -34,5 +35,6 @@ export const handlers = [
...limitAnalyseHandlers,
...posthogHandlers,
...externalHandlers,
...agentHandlers,
// ...userHandlers,
];

View File

@@ -21,11 +21,12 @@ import { NotificationProvider } from '../contexts/NotificationContext';
*
* Provider 层级顺序 (从外到内):
* 1. ReduxProvider - 状态管理层
* 2. ChakraProvider - UI 框架层
* 2. ChakraProvider - UI 框架层(主要)
* 3. NotificationProvider - 通知系统
* 4. AuthProvider - 认证系统
*
* 注意:
* - HeroUI v3 不再需要 HeroUIProvider样式通过 CSS 导入加载 (src/styles/heroui.css)
* - AuthModal 已迁移到 Redux (authModalSlice + useAuthModal Hook)
* - ErrorBoundary 在各 Layout 层实现,不在全局层,以实现精细化错误隔离
* - MainLayout: PageTransitionWrapper 包含 ErrorBoundary (页面错误不影响导航栏)
@@ -39,6 +40,13 @@ export function AppProviders({ children }) {
<ReduxProvider store={store}>
<ChakraProvider
theme={theme}
// ✅ 强制使用浅色主题(禁用深色模式)
colorModeManager={{
type: 'cookie',
ssr: false,
get: () => 'light', // 始终返回 'light'
set: () => {}, // 禁止设置(忽略切换操作)
}}
toastOptions={{
defaultOptions: {
position: 'top',

View File

@@ -102,17 +102,6 @@ export const homeRoutes = [
}
},
// 数据浏览器 - /home/data-browser
{
path: 'data-browser',
component: lazyComponents.DataBrowser,
protection: PROTECTION_MODES.MODAL,
meta: {
title: '数据浏览器',
description: '化工商品数据分类树浏览器'
}
},
// 回退路由 - 匹配任何未定义的 /home/* 路径
{
path: '*',

View File

@@ -11,7 +11,7 @@ export const lazyComponents = {
// Home 模块
HomePage: React.lazy(() => import('../views/Home/HomePage')),
CenterDashboard: React.lazy(() => import('../views/Dashboard/Center')),
ProfilePage: React.lazy(() => import('../views/Profile/ProfilePage')),
ProfilePage: React.lazy(() => import('../views/Profile')),
SettingsPage: React.lazy(() => import('../views/Settings/SettingsPage')),
Subscription: React.lazy(() => import('../views/Pages/Account/Subscription')),
PrivacyPolicy: React.lazy(() => import('../views/Pages/PrivacyPolicy')),
@@ -42,6 +42,7 @@ export const lazyComponents = {
// 价值论坛模块
ValueForum: React.lazy(() => import('../views/ValueForum')),
ForumPostDetail: React.lazy(() => import('../views/ValueForum/PostDetail')),
PredictionTopicDetail: React.lazy(() => import('../views/ValueForum/PredictionTopicDetail')),
// 数据浏览器模块
DataBrowser: React.lazy(() => import('../views/DataBrowser')),

View File

@@ -53,6 +53,16 @@ export const routeConfig = [
description: '热门概念追踪'
}
},
{
path: 'data-browser',
component: lazyComponents.DataBrowser,
protection: PROTECTION_MODES.MODAL,
layout: 'main',
meta: {
title: '数据浏览器',
description: '化工商品数据分类树浏览器'
}
},
{
path: 'stocks',
component: lazyComponents.StockOverview,
@@ -171,16 +181,26 @@ export const routeConfig = [
description: '论坛帖子详细内容'
}
},
{
path: 'value-forum/prediction/:topicId',
component: lazyComponents.PredictionTopicDetail,
protection: PROTECTION_MODES.MODAL,
layout: 'main',
meta: {
title: '预测话题详情',
description: '预测市场话题详细信息'
}
},
// ==================== Agent模块 ====================
{
path: 'agent-chat',
component: lazyComponents.AgentChat,
protection: PROTECTION_MODES.MODAL,
layout: 'main',
layout: 'main', // 使用主布局(带导航栏)
meta: {
title: '价小前投研',
description: '北京价值前沿科技公司的AI投研聊天助手'
title: '价小前投研 AI',
description: '超炫酷的 AI 投研聊天助手 - 基于 Hero UI'
}
},
];

View File

@@ -87,16 +87,18 @@ export const fetchCategoryTree = async (
* 获取特定节点及其子树
* @param path 节点完整路径(用 | 分隔)
* @param source 数据源类型 ('SMM' | 'Mysteel')
* @param maxDepth 返回的子树最大层级深度默认1层只返回直接子节点
* @returns 节点数据及其子树
*/
export const fetchCategoryNode = async (
path: string,
source: 'SMM' | 'Mysteel'
source: 'SMM' | 'Mysteel',
maxDepth: number = 1
): Promise<TreeNode> => {
try {
const encodedPath = encodeURIComponent(path);
const response = await fetch(
`/category-api/api/category-tree/node?path=${encodedPath}&source=${source}`,
`/category-api/api/category-tree/node?path=${encodedPath}&source=${source}&max_depth=${maxDepth}`,
{
method: 'GET',
headers: {

View File

@@ -0,0 +1,492 @@
/**
* 积分系统服务
* 管理用户积分账户、交易、奖励等
*/
// ==================== 常量配置 ====================
export const CREDIT_CONFIG = {
INITIAL_BALANCE: 10000, // 初始积分
MIN_BALANCE: 100, // 最低保留余额(破产保护)
MAX_SINGLE_BET: 1000, // 单次下注上限
DAILY_BONUS: 100, // 每日签到奖励
CREATE_TOPIC_COST: 100, // 创建话题费用
};
// 积分账户存储(生产环境应使用数据库)
const userAccounts = new Map();
// 交易记录存储
const transactions = [];
// ==================== 账户管理 ====================
/**
* 获取用户账户
* @param {string} userId - 用户ID
* @returns {Object} 用户账户信息
*/
export const getUserAccount = (userId) => {
if (!userAccounts.has(userId)) {
// 首次访问,创建新账户
const newAccount = {
user_id: userId,
balance: CREDIT_CONFIG.INITIAL_BALANCE,
frozen: 0,
total: CREDIT_CONFIG.INITIAL_BALANCE,
total_earned: CREDIT_CONFIG.INITIAL_BALANCE,
total_spent: 0,
total_profit: 0,
active_positions: [],
stats: {
total_topics: 0,
win_count: 0,
loss_count: 0,
win_rate: 0,
best_profit: 0,
},
last_daily_bonus: null,
};
userAccounts.set(userId, newAccount);
}
return userAccounts.get(userId);
};
/**
* 更新用户账户
* @param {string} userId - 用户ID
* @param {Object} updates - 更新内容
*/
export const updateUserAccount = (userId, updates) => {
const account = getUserAccount(userId);
const updated = { ...account, ...updates };
userAccounts.set(userId, updated);
return updated;
};
/**
* 获取用户积分余额
* @param {string} userId - 用户ID
* @returns {number} 可用余额
*/
export const getBalance = (userId) => {
const account = getUserAccount(userId);
return account.balance;
};
/**
* 检查用户是否能支付
* @param {string} userId - 用户ID
* @param {number} amount - 金额
* @returns {boolean} 是否能支付
*/
export const canAfford = (userId, amount) => {
const account = getUserAccount(userId);
const afterBalance = account.balance - amount;
// 必须保留最低余额
return afterBalance >= CREDIT_CONFIG.MIN_BALANCE;
};
// ==================== 积分操作 ====================
/**
* 增加积分
* @param {string} userId - 用户ID
* @param {number} amount - 金额
* @param {string} reason - 原因
*/
export const addCredits = (userId, amount, reason = '系统增加') => {
const account = getUserAccount(userId);
const updated = {
balance: account.balance + amount,
total: account.total + amount,
total_earned: account.total_earned + amount,
};
updateUserAccount(userId, updated);
// 记录交易
logTransaction({
user_id: userId,
type: 'earn',
amount,
reason,
balance_after: updated.balance,
});
return updated;
};
/**
* 扣除积分
* @param {string} userId - 用户ID
* @param {number} amount - 金额
* @param {string} reason - 原因
* @throws {Error} 如果余额不足
*/
export const deductCredits = (userId, amount, reason = '系统扣除') => {
if (!canAfford(userId, amount)) {
throw new Error(`积分不足,需要${amount}积分,但只有${getBalance(userId)}积分`);
}
const account = getUserAccount(userId);
const updated = {
balance: account.balance - amount,
total_spent: account.total_spent + amount,
};
updateUserAccount(userId, updated);
// 记录交易
logTransaction({
user_id: userId,
type: 'spend',
amount: -amount,
reason,
balance_after: updated.balance,
});
return updated;
};
/**
* 冻结积分(席位占用)
* @param {string} userId - 用户ID
* @param {number} amount - 金额
*/
export const freezeCredits = (userId, amount) => {
const account = getUserAccount(userId);
if (account.balance < amount) {
throw new Error('可用余额不足');
}
const updated = {
balance: account.balance - amount,
frozen: account.frozen + amount,
};
updateUserAccount(userId, updated);
return updated;
};
/**
* 解冻积分
* @param {string} userId - 用户ID
* @param {number} amount - 金额
*/
export const unfreezeCredits = (userId, amount) => {
const account = getUserAccount(userId);
const updated = {
balance: account.balance + amount,
frozen: account.frozen - amount,
};
updateUserAccount(userId, updated);
return updated;
};
// ==================== 每日奖励 ====================
/**
* 领取每日签到奖励
* @param {string} userId - 用户ID
* @returns {Object} 奖励信息
*/
export const claimDailyBonus = (userId) => {
const account = getUserAccount(userId);
const today = new Date().toDateString();
// 检查是否已领取
if (account.last_daily_bonus === today) {
return {
success: false,
message: '今日已领取',
};
}
// 发放奖励
addCredits(userId, CREDIT_CONFIG.DAILY_BONUS, '每日签到');
// 更新领取时间
updateUserAccount(userId, { last_daily_bonus: today });
return {
success: true,
amount: CREDIT_CONFIG.DAILY_BONUS,
message: `获得${CREDIT_CONFIG.DAILY_BONUS}积分`,
};
};
/**
* 检查今天是否已签到
* @param {string} userId - 用户ID
* @returns {boolean}
*/
export const hasClaimedToday = (userId) => {
const account = getUserAccount(userId);
const today = new Date().toDateString();
return account.last_daily_bonus === today;
};
// ==================== 持仓管理 ====================
/**
* 添加持仓
* @param {string} userId - 用户ID
* @param {Object} position - 持仓信息
*/
export const addPosition = (userId, position) => {
const account = getUserAccount(userId);
const updated = {
active_positions: [...account.active_positions, position],
stats: {
...account.stats,
total_topics: account.stats.total_topics + 1,
},
};
updateUserAccount(userId, updated);
return updated;
};
/**
* 移除持仓
* @param {string} userId - 用户ID
* @param {string} positionId - 持仓ID
*/
export const removePosition = (userId, positionId) => {
const account = getUserAccount(userId);
const updated = {
active_positions: account.active_positions.filter((p) => p.id !== positionId),
};
updateUserAccount(userId, updated);
return updated;
};
/**
* 更新持仓
* @param {string} userId - 用户ID
* @param {string} positionId - 持仓ID
* @param {Object} updates - 更新内容
*/
export const updatePosition = (userId, positionId, updates) => {
const account = getUserAccount(userId);
const updated = {
active_positions: account.active_positions.map((p) =>
p.id === positionId ? { ...p, ...updates } : p
),
};
updateUserAccount(userId, updated);
return updated;
};
/**
* 获取用户持仓
* @param {string} userId - 用户ID
* @param {string} topicId - 话题ID可选
* @returns {Array} 持仓列表
*/
export const getUserPositions = (userId, topicId = null) => {
const account = getUserAccount(userId);
if (topicId) {
return account.active_positions.filter((p) => p.topic_id === topicId);
}
return account.active_positions;
};
// ==================== 统计更新 ====================
/**
* 记录胜利
* @param {string} userId - 用户ID
* @param {number} profit - 盈利金额
*/
export const recordWin = (userId, profit) => {
const account = getUserAccount(userId);
const newWinCount = account.stats.win_count + 1;
const totalGames = newWinCount + account.stats.loss_count;
const winRate = (newWinCount / totalGames) * 100;
const updated = {
total_profit: account.total_profit + profit,
stats: {
...account.stats,
win_count: newWinCount,
win_rate: winRate,
best_profit: Math.max(account.stats.best_profit, profit),
},
};
updateUserAccount(userId, updated);
return updated;
};
/**
* 记录失败
* @param {string} userId - 用户ID
* @param {number} loss - 损失金额
*/
export const recordLoss = (userId, loss) => {
const account = getUserAccount(userId);
const newLossCount = account.stats.loss_count + 1;
const totalGames = account.stats.win_count + newLossCount;
const winRate = (account.stats.win_count / totalGames) * 100;
const updated = {
total_profit: account.total_profit - loss,
stats: {
...account.stats,
loss_count: newLossCount,
win_rate: winRate,
},
};
updateUserAccount(userId, updated);
return updated;
};
// ==================== 排行榜 ====================
/**
* 获取积分排行榜
* @param {number} limit - 返回数量
* @returns {Array} 排行榜数据
*/
export const getLeaderboard = (limit = 100) => {
const accounts = Array.from(userAccounts.values());
return accounts
.sort((a, b) => b.total - a.total)
.slice(0, limit)
.map((account, index) => ({
rank: index + 1,
user_id: account.user_id,
total: account.total,
total_profit: account.total_profit,
win_rate: account.stats.win_rate,
}));
};
/**
* 获取用户排名
* @param {string} userId - 用户ID
* @returns {number} 排名
*/
export const getUserRank = (userId) => {
const leaderboard = getLeaderboard(1000);
const index = leaderboard.findIndex((item) => item.user_id === userId);
return index >= 0 ? index + 1 : -1;
};
// ==================== 交易记录 ====================
/**
* 记录交易
* @param {Object} transaction - 交易信息
*/
const logTransaction = (transaction) => {
const record = {
id: `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
...transaction,
};
transactions.push(record);
return record;
};
/**
* 获取用户交易记录
* @param {string} userId - 用户ID
* @param {number} limit - 返回数量
* @returns {Array} 交易记录
*/
export const getUserTransactions = (userId, limit = 50) => {
return transactions
.filter((tx) => tx.user_id === userId)
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
.slice(0, limit);
};
// ==================== 批量操作 ====================
/**
* 批量发放积分(如活动奖励)
* @param {Array} recipients - [{user_id, amount, reason}]
*/
export const batchAddCredits = (recipients) => {
const results = recipients.map(({ user_id, amount, reason }) => {
try {
return {
user_id,
success: true,
account: addCredits(user_id, amount, reason),
};
} catch (error) {
return {
user_id,
success: false,
error: error.message,
};
}
});
return results;
};
// ==================== 导出所有功能 ====================
export default {
CREDIT_CONFIG,
// 账户管理
getUserAccount,
updateUserAccount,
getBalance,
canAfford,
// 积分操作
addCredits,
deductCredits,
freezeCredits,
unfreezeCredits,
// 每日奖励
claimDailyBonus,
hasClaimedToday,
// 持仓管理
addPosition,
removePosition,
updatePosition,
getUserPositions,
// 统计更新
recordWin,
recordLoss,
// 排行榜
getLeaderboard,
getUserRank,
// 交易记录
getUserTransactions,
// 批量操作
batchAddCredits,
};

View File

@@ -0,0 +1,325 @@
/**
* 预测市场服务 - API 版本
* 调用真实的后端 API数据存储到 MySQL 数据库
*/
import axios from 'axios';
import { getApiBase } from '@utils/apiConfig';
const api = axios.create({
baseURL: getApiBase(),
timeout: 10000,
withCredentials: true, // 携带 Cookiesession
});
// ==================== 积分系统 API ====================
/**
* 获取用户积分账户
*/
export const getUserAccount = async () => {
try {
const response = await api.get('/api/prediction/credit/account');
return response.data;
} catch (error) {
console.error('获取积分账户失败:', error);
throw error;
}
};
/**
* 领取每日奖励100积分
*/
export const claimDailyBonus = async () => {
try {
const response = await api.post('/api/prediction/credit/daily-bonus');
return response.data;
} catch (error) {
console.error('领取每日奖励失败:', error);
throw error;
}
};
// ==================== 预测话题 API ====================
/**
* 创建预测话题
* @param {Object} topicData - { title, description, category, deadline }
*/
export const createTopic = async (topicData) => {
try {
const response = await api.post('/api/prediction/topics', topicData);
return response.data;
} catch (error) {
console.error('创建预测话题失败:', error);
throw error;
}
};
/**
* 获取预测话题列表
* @param {Object} params - { status, category, sort_by, page, per_page }
*/
export const getTopics = async (params = {}) => {
try {
const response = await api.get('/api/prediction/topics', { params });
return response.data;
} catch (error) {
console.error('获取话题列表失败:', error);
throw error;
}
};
/**
* 获取预测话题详情
* @param {number} topicId
*/
export const getTopicDetail = async (topicId) => {
try {
const response = await api.get(`/api/prediction/topics/${topicId}`);
return response.data;
} catch (error) {
console.error('获取话题详情失败:', error);
throw error;
}
};
/**
* 结算预测话题(仅创建者可操作)
* @param {number} topicId
* @param {string} result - 'yes' | 'no' | 'draw'
*/
export const settleTopic = async (topicId, result) => {
try {
const response = await api.post(`/api/prediction/topics/${topicId}/settle`, { result });
return response.data;
} catch (error) {
console.error('结算话题失败:', error);
throw error;
}
};
// ==================== 交易 API ====================
/**
* 买入预测份额
* @param {Object} tradeData - { topic_id, direction, shares }
*/
export const buyShares = async (tradeData) => {
try {
const response = await api.post('/api/prediction/trade/buy', tradeData);
return response.data;
} catch (error) {
console.error('买入份额失败:', error);
throw error;
}
};
/**
* 获取用户持仓列表
*/
export const getUserPositions = async () => {
try {
const response = await api.get('/api/prediction/positions');
return response.data;
} catch (error) {
console.error('获取持仓列表失败:', error);
throw error;
}
};
// ==================== 评论 API ====================
/**
* 发表话题评论
* @param {number} topicId
* @param {Object} commentData - { content, parent_id }
*/
export const createComment = async (topicId, commentData) => {
try {
const response = await api.post(`/api/prediction/topics/${topicId}/comments`, commentData);
return response.data;
} catch (error) {
console.error('发表评论失败:', error);
throw error;
}
};
/**
* 获取话题评论列表
* @param {number} topicId
* @param {Object} params - { page, per_page }
*/
export const getComments = async (topicId, params = {}) => {
try {
const response = await api.get(`/api/prediction/topics/${topicId}/comments`, { params });
return response.data;
} catch (error) {
console.error('获取评论列表失败:', error);
throw error;
}
};
/**
* 点赞/取消点赞评论
* @param {number} commentId
*/
export const likeComment = async (commentId) => {
try {
const response = await api.post(`/api/prediction/comments/${commentId}/like`);
return response.data;
} catch (error) {
console.error('点赞评论失败:', error);
throw error;
}
};
// ==================== 观点IPO API ====================
/**
* 投资评论观点IPO
* @param {number} commentId - 评论ID
* @param {number} shares - 投资份额
*/
export const investComment = async (commentId, shares) => {
try {
const response = await api.post(`/api/prediction/comments/${commentId}/invest`, { shares });
return response.data;
} catch (error) {
console.error('投资评论失败:', error);
throw error;
}
};
/**
* 获取评论的投资列表
* @param {number} commentId - 评论ID
*/
export const getCommentInvestments = async (commentId) => {
try {
const response = await api.get(`/api/prediction/comments/${commentId}/investments`);
return response.data;
} catch (error) {
console.error('获取投资列表失败:', error);
throw error;
}
};
/**
* 验证评论结果(仅创建者可操作)
* @param {number} commentId - 评论ID
* @param {string} result - 'correct' | 'incorrect'
*/
export const verifyComment = async (commentId, result) => {
try {
const response = await api.post(`/api/prediction/comments/${commentId}/verify`, { result });
return response.data;
} catch (error) {
console.error('验证评论失败:', error);
throw error;
}
};
// ==================== 工具函数(价格计算保留在前端,用于实时预览)====================
export const MARKET_CONFIG = {
MAX_SEATS_PER_SIDE: 5,
TAX_RATE: 0.02,
MIN_PRICE: 50,
MAX_PRICE: 950,
BASE_PRICE: 500,
};
/**
* 计算当前价格简化版AMM
* @param {number} yesShares - Yes方总份额
* @param {number} noShares - No方总份额
* @returns {Object} {yes: price, no: price}
*/
export const calculatePrice = (yesShares, noShares) => {
const totalShares = yesShares + noShares;
if (totalShares === 0) {
return {
yes: MARKET_CONFIG.BASE_PRICE,
no: MARKET_CONFIG.BASE_PRICE,
};
}
const yesProb = yesShares / totalShares;
const noProb = noShares / totalShares;
let yesPrice = yesProb * 1000;
let noPrice = noProb * 1000;
yesPrice = Math.max(MARKET_CONFIG.MIN_PRICE, Math.min(MARKET_CONFIG.MAX_PRICE, yesPrice));
noPrice = Math.max(MARKET_CONFIG.MIN_PRICE, Math.min(MARKET_CONFIG.MAX_PRICE, noPrice));
return { yes: Math.round(yesPrice), no: Math.round(noPrice) };
};
/**
* 计算交易税
* @param {number} amount - 交易金额
* @returns {number} 税费
*/
export const calculateTax = (amount) => {
return Math.floor(amount * MARKET_CONFIG.TAX_RATE);
};
/**
* 计算买入成本(用于前端预览)
* @param {number} currentShares - 当前方总份额
* @param {number} otherShares - 对手方总份额
* @param {number} buyAmount - 买入数量
* @returns {Object} { amount, tax, total }
*/
export const calculateBuyCost = (currentShares, otherShares, buyAmount) => {
const currentPrice = calculatePrice(currentShares, otherShares);
const afterShares = currentShares + buyAmount;
const afterPrice = calculatePrice(afterShares, otherShares);
const avgPrice = (currentPrice.yes + afterPrice.yes) / 2;
const amount = avgPrice * buyAmount;
const tax = calculateTax(amount);
const total = amount + tax;
return {
amount: Math.round(amount),
tax: Math.round(tax),
total: Math.round(total),
avgPrice: Math.round(avgPrice),
};
};
export default {
// 积分系统
getUserAccount,
claimDailyBonus,
// 话题管理
createTopic,
getTopics,
getTopicDetail,
settleTopic,
// 交易
buyShares,
getUserPositions,
// 评论
createComment,
getComments,
likeComment,
// 观点IPO
investComment,
getCommentInvestments,
verifyComment,
// 工具函数
calculatePrice,
calculateTax,
calculateBuyCost,
MARKET_CONFIG,
};

View File

@@ -0,0 +1,738 @@
/**
* 预测市场服务
* 核心功能:话题管理、席位交易、动态定价、领主系统、奖池分配
*/
import {
addCredits,
deductCredits,
canAfford,
addPosition,
removePosition,
updatePosition,
getUserPositions,
recordWin,
recordLoss,
CREDIT_CONFIG,
} from './creditSystemService';
// ==================== 常量配置 ====================
export const MARKET_CONFIG = {
MAX_SEATS_PER_SIDE: 5, // 每个方向最多5个席位
TAX_RATE: 0.02, // 交易税率 2%
MIN_PRICE: 50, // 最低价格
MAX_PRICE: 950, // 最高价格
BASE_PRICE: 500, // 基础价格
};
// 话题存储生产环境应使用Elasticsearch
const topics = new Map();
// 席位存储
const positions = new Map();
// 交易记录
const trades = [];
// ==================== 动态定价算法 ====================
/**
* 计算当前价格简化版AMM
* @param {number} yesShares - Yes方总份额
* @param {number} noShares - No方总份额
* @returns {Object} {yes: price, no: price}
*/
export const calculatePrice = (yesShares, noShares) => {
const totalShares = yesShares + noShares;
if (totalShares === 0) {
// 初始状态,双方价格相同
return {
yes: MARKET_CONFIG.BASE_PRICE,
no: MARKET_CONFIG.BASE_PRICE,
};
}
// 概率加权定价
const yesProb = yesShares / totalShares;
const noProb = noShares / totalShares;
// 价格 = 概率 * 1000限制在 [MIN_PRICE, MAX_PRICE]
const yesPrice = Math.max(
MARKET_CONFIG.MIN_PRICE,
Math.min(MARKET_CONFIG.MAX_PRICE, yesProb * 1000)
);
const noPrice = Math.max(
MARKET_CONFIG.MIN_PRICE,
Math.min(MARKET_CONFIG.MAX_PRICE, noProb * 1000)
);
return { yes: yesPrice, no: noPrice };
};
/**
* 计算购买成本(含滑点)
* @param {number} currentShares - 当前份额
* @param {number} otherShares - 对手方份额
* @param {number} buyAmount - 购买数量
* @returns {number} 总成本
*/
export const calculateBuyCost = (currentShares, otherShares, buyAmount) => {
let totalCost = 0;
let tempShares = currentShares;
// 模拟逐步购买,累计成本
for (let i = 0; i < buyAmount; i++) {
tempShares += 1;
const prices = calculatePrice(tempShares, otherShares);
// 假设购买的是yes方
totalCost += prices.yes;
}
return totalCost;
};
/**
* 计算卖出收益(含滑点)
* @param {number} currentShares - 当前份额
* @param {number} otherShares - 对手方份额
* @param {number} sellAmount - 卖出数量
* @returns {number} 总收益
*/
export const calculateSellRevenue = (currentShares, otherShares, sellAmount) => {
let totalRevenue = 0;
let tempShares = currentShares;
// 模拟逐步卖出,累计收益
for (let i = 0; i < sellAmount; i++) {
const prices = calculatePrice(tempShares, otherShares);
totalRevenue += prices.yes;
tempShares -= 1;
}
return totalRevenue;
};
/**
* 计算交易税
* @param {number} amount - 交易金额
* @returns {number} 税费
*/
export const calculateTax = (amount) => {
return Math.floor(amount * MARKET_CONFIG.TAX_RATE);
};
// ==================== 话题管理 ====================
/**
* 创建预测话题
* @param {Object} topicData - 话题数据
* @returns {Object} 创建的话题
*/
export const createTopic = (topicData) => {
const { author_id, title, description, category, tags, deadline, settlement_date } = topicData;
// 扣除创建费用
deductCredits(author_id, CREDIT_CONFIG.CREATE_TOPIC_COST, '创建预测话题');
const topic = {
id: `topic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: 'prediction',
// 基础信息
title,
description,
category,
tags: tags || [],
// 作者信息
author_id,
author_name: topicData.author_name,
author_avatar: topicData.author_avatar,
// 时间管理
created_at: new Date().toISOString(),
deadline: deadline || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 默认7天
settlement_date: settlement_date || new Date(Date.now() + 8 * 24 * 60 * 60 * 1000).toISOString(),
status: 'active',
// 预测选项
options: [
{ id: 'yes', label: '看涨 / Yes', color: '#48BB78' },
{ id: 'no', label: '看跌 / No', color: '#F56565' },
],
// 市场数据
total_pool: CREDIT_CONFIG.CREATE_TOPIC_COST, // 创建费用进入奖池
tax_rate: MARKET_CONFIG.TAX_RATE,
// 席位数据
positions: {
yes: {
seats: [],
total_shares: 0,
current_price: MARKET_CONFIG.BASE_PRICE,
lord_id: null,
},
no: {
seats: [],
total_shares: 0,
current_price: MARKET_CONFIG.BASE_PRICE,
lord_id: null,
},
},
// 交易统计
stats: {
total_volume: 0,
total_transactions: 0,
unique_traders: new Set(),
},
// 结果
settlement: {
result: null,
evidence: null,
settled_by: null,
settled_at: null,
},
};
topics.set(topic.id, topic);
return topic;
};
/**
* 获取话题详情
* @param {string} topicId - 话题ID
* @returns {Object} 话题详情
*/
export const getTopic = (topicId) => {
return topics.get(topicId);
};
/**
* 更新话题
* @param {string} topicId - 话题ID
* @param {Object} updates - 更新内容
*/
export const updateTopic = (topicId, updates) => {
const topic = getTopic(topicId);
const updated = { ...topic, ...updates };
topics.set(topicId, updated);
return updated;
};
/**
* 获取所有话题列表
* @param {Object} filters - 筛选条件
* @returns {Array} 话题列表
*/
export const getTopics = (filters = {}) => {
let topicList = Array.from(topics.values());
// 按状态筛选
if (filters.status) {
topicList = topicList.filter((t) => t.status === filters.status);
}
// 按分类筛选
if (filters.category) {
topicList = topicList.filter((t) => t.category === filters.category);
}
// 排序
const sortBy = filters.sortBy || 'created_at';
topicList.sort((a, b) => {
if (sortBy === 'created_at') {
return new Date(b.created_at) - new Date(a.created_at);
}
if (sortBy === 'total_pool') {
return b.total_pool - a.total_pool;
}
if (sortBy === 'total_volume') {
return b.stats.total_volume - a.stats.total_volume;
}
return 0;
});
return topicList;
};
// ==================== 席位管理 ====================
/**
* 创建席位
* @param {Object} positionData - 席位数据
* @returns {Object} 创建的席位
*/
const createPosition = (positionData) => {
const position = {
id: `pos_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
...positionData,
acquired_at: new Date().toISOString(),
last_traded_at: new Date().toISOString(),
is_lord: false,
};
positions.set(position.id, position);
return position;
};
/**
* 获取席位
* @param {string} positionId - 席位ID
* @returns {Object} 席位信息
*/
export const getPosition = (positionId) => {
return positions.get(positionId);
};
/**
* 分配席位取份额最高的前5名
* @param {Array} allPositions - 所有持仓
* @returns {Array} 席位列表
*/
const allocateSeats = (allPositions) => {
// 按份额排序
const sorted = [...allPositions].sort((a, b) => b.shares - a.shares);
// 取前5名
return sorted.slice(0, MARKET_CONFIG.MAX_SEATS_PER_SIDE);
};
/**
* 确定领主(份额最多的人)
* @param {Array} seats - 席位列表
* @returns {string|null} 领主用户ID
*/
const determineLord = (seats) => {
if (seats.length === 0) return null;
const lord = seats.reduce((max, seat) => (seat.shares > max.shares ? seat : max));
return lord.holder_id;
};
/**
* 更新领主标识
* @param {string} topicId - 话题ID
* @param {string} optionId - 选项ID
*/
const updateLordStatus = (topicId, optionId) => {
const topic = getTopic(topicId);
const sideData = topic.positions[optionId];
// 重新分配席位
const allPositions = Array.from(positions.values()).filter(
(p) => p.topic_id === topicId && p.option_id === optionId
);
const seats = allocateSeats(allPositions);
const lordId = determineLord(seats);
// 更新所有席位的领主标识
allPositions.forEach((position) => {
const isLord = position.holder_id === lordId;
positions.set(position.id, { ...position, is_lord: isLord });
});
// 更新话题数据
updateTopic(topicId, {
positions: {
...topic.positions,
[optionId]: {
...sideData,
seats,
lord_id: lordId,
},
},
});
return lordId;
};
// ==================== 交易执行 ====================
/**
* 购买席位
* @param {Object} tradeData - 交易数据
* @returns {Object} 交易结果
*/
export const buyPosition = (tradeData) => {
const { user_id, user_name, user_avatar, topic_id, option_id, shares } = tradeData;
// 验证
const topic = getTopic(topic_id);
if (!topic) throw new Error('话题不存在');
if (topic.status !== 'active') throw new Error('话题已关闭交易');
if (topic.author_id === user_id) throw new Error('不能参与自己发起的话题');
// 检查购买上限
if (shares * MARKET_CONFIG.BASE_PRICE > CREDIT_CONFIG.MAX_SINGLE_BET) {
throw new Error(`单次购买上限为${CREDIT_CONFIG.MAX_SINGLE_BET}积分`);
}
// 获取当前市场数据
const sideData = topic.positions[option_id];
const otherOptionId = option_id === 'yes' ? 'no' : 'yes';
const otherSideData = topic.positions[otherOptionId];
// 计算成本
const cost = calculateBuyCost(sideData.total_shares, otherSideData.total_shares, shares);
const tax = calculateTax(cost);
const totalCost = cost + tax;
// 检查余额
if (!canAfford(user_id, totalCost)) {
throw new Error(`积分不足,需要${totalCost}积分`);
}
// 扣除积分
deductCredits(user_id, totalCost, `购买预测席位 - ${topic.title}`);
// 税费进入奖池
updateTopic(topic_id, {
total_pool: topic.total_pool + tax,
stats: {
...topic.stats,
total_volume: topic.stats.total_volume + totalCost,
total_transactions: topic.stats.total_transactions + 1,
unique_traders: topic.stats.unique_traders.add(user_id),
},
});
// 查找用户是否已有该选项的席位
let userPosition = Array.from(positions.values()).find(
(p) => p.topic_id === topic_id && p.option_id === option_id && p.holder_id === user_id
);
if (userPosition) {
// 更新现有席位
const newShares = userPosition.shares + shares;
const newAvgCost = (userPosition.avg_cost * userPosition.shares + cost) / newShares;
positions.set(userPosition.id, {
...userPosition,
shares: newShares,
avg_cost: newAvgCost,
last_traded_at: new Date().toISOString(),
});
// 更新用户账户持仓
updatePosition(user_id, userPosition.id, {
shares: newShares,
avg_cost: newAvgCost,
});
} else {
// 创建新席位
const newPosition = createPosition({
topic_id,
option_id,
holder_id: user_id,
holder_name: user_name,
holder_avatar: user_avatar,
shares,
avg_cost: cost / shares,
current_value: cost,
unrealized_pnl: 0,
});
// 添加到用户账户
addPosition(user_id, {
id: newPosition.id,
topic_id,
option_id,
shares,
avg_cost: cost / shares,
});
userPosition = newPosition;
}
// 更新话题席位数据
updateTopic(topic_id, {
positions: {
...topic.positions,
[option_id]: {
...sideData,
total_shares: sideData.total_shares + shares,
},
},
});
// 更新价格
const newPrices = calculatePrice(
topic.positions[option_id].total_shares + shares,
topic.positions[otherOptionId].total_shares
);
updateTopic(topic_id, {
positions: {
...topic.positions,
yes: { ...topic.positions.yes, current_price: newPrices.yes },
no: { ...topic.positions.no, current_price: newPrices.no },
},
});
// 更新领主状态
const newLordId = updateLordStatus(topic_id, option_id);
// 记录交易
const trade = {
id: `trade_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
topic_id,
option_id,
buyer_id: user_id,
seller_id: null,
type: 'buy',
shares,
price: cost / shares,
total_cost: totalCost,
tax,
created_at: new Date().toISOString(),
};
trades.push(trade);
return {
success: true,
position: userPosition,
trade,
new_lord_id: newLordId,
current_price: newPrices[option_id],
};
};
/**
* 卖出席位
* @param {Object} tradeData - 交易数据
* @returns {Object} 交易结果
*/
export const sellPosition = (tradeData) => {
const { user_id, topic_id, option_id, shares } = tradeData;
// 验证
const topic = getTopic(topic_id);
if (!topic) throw new Error('话题不存在');
if (topic.status !== 'active') throw new Error('话题已关闭交易');
// 查找用户席位
const userPosition = Array.from(positions.values()).find(
(p) => p.topic_id === topic_id && p.option_id === option_id && p.holder_id === user_id
);
if (!userPosition) throw new Error('未持有该席位');
if (userPosition.shares < shares) throw new Error('持有份额不足');
// 获取当前市场数据
const sideData = topic.positions[option_id];
const otherOptionId = option_id === 'yes' ? 'no' : 'yes';
const otherSideData = topic.positions[otherOptionId];
// 计算收益
const revenue = calculateSellRevenue(sideData.total_shares, otherSideData.total_shares, shares);
const tax = calculateTax(revenue);
const netRevenue = revenue - tax;
// 返还积分
addCredits(user_id, netRevenue, `卖出预测席位 - ${topic.title}`);
// 税费进入奖池
updateTopic(topic_id, {
total_pool: topic.total_pool + tax,
stats: {
...topic.stats,
total_volume: topic.stats.total_volume + revenue,
total_transactions: topic.stats.total_transactions + 1,
},
});
// 更新席位
const newShares = userPosition.shares - shares;
if (newShares === 0) {
// 完全卖出,删除席位
positions.delete(userPosition.id);
removePosition(user_id, userPosition.id);
} else {
// 部分卖出,更新份额
positions.set(userPosition.id, {
...userPosition,
shares: newShares,
last_traded_at: new Date().toISOString(),
});
updatePosition(user_id, userPosition.id, { shares: newShares });
}
// 更新话题席位数据
updateTopic(topic_id, {
positions: {
...topic.positions,
[option_id]: {
...sideData,
total_shares: sideData.total_shares - shares,
},
},
});
// 更新价格
const newPrices = calculatePrice(
topic.positions[option_id].total_shares - shares,
topic.positions[otherOptionId].total_shares
);
updateTopic(topic_id, {
positions: {
...topic.positions,
yes: { ...topic.positions.yes, current_price: newPrices.yes },
no: { ...topic.positions.no, current_price: newPrices.no },
},
});
// 更新领主状态
const newLordId = updateLordStatus(topic_id, option_id);
// 记录交易
const trade = {
id: `trade_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
topic_id,
option_id,
buyer_id: null,
seller_id: user_id,
type: 'sell',
shares,
price: revenue / shares,
total_cost: netRevenue,
tax,
created_at: new Date().toISOString(),
};
trades.push(trade);
return {
success: true,
trade,
new_lord_id: newLordId,
current_price: newPrices[option_id],
};
};
// ==================== 结算 ====================
/**
* 结算话题
* @param {string} topicId - 话题ID
* @param {string} result - 结果 'yes' | 'no'
* @param {string} evidence - 证据说明
* @param {string} settledBy - 裁决者ID
* @returns {Object} 结算结果
*/
export const settleTopic = (topicId, result, evidence, settledBy) => {
const topic = getTopic(topicId);
if (!topic) throw new Error('话题不存在');
if (topic.status === 'settled') throw new Error('话题已结算');
// 只有作者可以结算
if (topic.author_id !== settledBy) throw new Error('无权结算');
// 获取获胜方和失败方
const winningOption = result;
const losingOption = result === 'yes' ? 'no' : 'yes';
const winners = Array.from(positions.values()).filter(
(p) => p.topic_id === topicId && p.option_id === winningOption
);
const losers = Array.from(positions.values()).filter(
(p) => p.topic_id === topicId && p.option_id === losingOption
);
// 分配奖池
if (winners.length === 0) {
// 无人获胜,奖池返还给作者
addCredits(topic.author_id, topic.total_pool, '话题奖池返还');
} else {
// 计算获胜方总份额
const totalWinningShares = winners.reduce((sum, p) => sum + p.shares, 0);
// 按份额分配
winners.forEach((position) => {
const share = position.shares / totalWinningShares;
const reward = Math.floor(topic.total_pool * share);
// 返还本金 + 奖池分成
const refund = Math.floor(position.avg_cost * position.shares);
const total = refund + reward;
addCredits(position.holder_id, total, `预测获胜 - ${topic.title}`);
// 记录胜利
recordWin(position.holder_id, reward);
// 删除席位
positions.delete(position.id);
removePosition(position.holder_id, position.id);
});
}
// 失败方损失本金
losers.forEach((position) => {
const loss = Math.floor(position.avg_cost * position.shares);
// 记录失败
recordLoss(position.holder_id, loss);
// 删除席位
positions.delete(position.id);
removePosition(position.holder_id, position.id);
});
// 更新话题状态
updateTopic(topicId, {
status: 'settled',
settlement: {
result,
evidence,
settled_by: settledBy,
settled_at: new Date().toISOString(),
},
});
return {
success: true,
winners_count: winners.length,
losers_count: losers.length,
total_distributed: topic.total_pool,
};
};
// ==================== 数据导出 ====================
export default {
MARKET_CONFIG,
// 定价算法
calculatePrice,
calculateBuyCost,
calculateSellRevenue,
calculateTax,
// 话题管理
createTopic,
getTopic,
updateTopic,
getTopics,
// 席位管理
getPosition,
// 交易
buyPosition,
sellPosition,
// 结算
settleTopic,
};

View File

@@ -7,7 +7,10 @@ import { io } from 'socket.io-client';
import { logger } from '../utils/logger';
import { getApiBase } from '../utils/apiConfig';
const API_BASE_URL = getApiBase();
// 优先使用 REACT_APP_SOCKET_URL专门为 Socket.IO 配置)
// 如果未配置,则使用 getApiBase()(与 HTTP API 共用地址)
// Mock 模式下可以通过 .env.mock 配置 REACT_APP_SOCKET_URL=https://valuefrontier.cn 连接生产环境
const API_BASE_URL = process.env.REACT_APP_SOCKET_URL || getApiBase();
class SocketService {
constructor() {

View File

@@ -2,7 +2,6 @@
// 认证弹窗状态管理 Redux Slice - 从 AuthModalContext 迁移
import { createSlice } from '@reduxjs/toolkit';
import { logger } from '../../utils/logger';
/**
* AuthModal Slice
@@ -22,9 +21,6 @@ const authModalSlice = createSlice({
openModal: (state, action) => {
state.isOpen = true;
state.redirectUrl = action.payload?.redirectUrl || null;
logger.debug('authModalSlice', '打开认证弹窗', {
redirectUrl: action.payload?.redirectUrl || '无'
});
},
/**
@@ -33,7 +29,6 @@ const authModalSlice = createSlice({
closeModal: (state) => {
state.isOpen = false;
state.redirectUrl = null;
logger.debug('authModalSlice', '关闭认证弹窗');
},
/**

View File

@@ -1,49 +0,0 @@
/* Brainwave 色彩变量定义 */
:root {
/* Brainwave 中性色系 */
--color-n-1: #FFFFFF;
--color-n-2: #CAC6DD;
--color-n-3: #ADA8C3;
--color-n-4: #757185;
--color-n-5: #3F3A52;
--color-n-6: #252134;
--color-n-7: #15131D;
--color-n-8: #0E0C15;
/* Brainwave 主题色 */
--color-1: #AC6AFF;
--color-2: #FFC876;
--color-3: #FF776F;
--color-4: #7ADB78;
--color-5: #858DFF;
--color-6: #FF98E2;
/* 描边色 */
--stroke-1: #26242C;
}
/* CSS类名映射到变量 */
.bg-n-8 { background-color: var(--color-n-8) !important; }
.bg-n-7 { background-color: var(--color-n-7) !important; }
.bg-n-6 { background-color: var(--color-n-6) !important; }
.text-n-1 { color: var(--color-n-1) !important; }
.text-n-2 { color: var(--color-n-2) !important; }
.text-n-3 { color: var(--color-n-3) !important; }
.text-n-4 { color: var(--color-n-4) !important; }
.border-n-6 { border-color: var(--color-n-6) !important; }
.border-n-1\/10 { border-color: rgba(255, 255, 255, 0.1) !important; }
.border-n-2\/5 { border-color: rgba(202, 198, 221, 0.05) !important; }
.border-n-2\/10 { border-color: rgba(202, 198, 221, 0.1) !important; }
.bg-stroke-1 { background-color: var(--stroke-1) !important; }
/* 渐变背景 */
.bg-conic-gradient {
background: conic-gradient(from 225deg, #FFC876, #79FFF7, #9F53FF, #FF98E2, #FFC876) !important;
}
.bg-gradient-to-br {
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)) !important;
}

View File

@@ -1,35 +1,12 @@
/* Tailwind CSS 入口文件 */
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
}
/* styles for splide carousel */
@layer components {
.splide-custom .splide__arrow {
@apply relative top-0 left-0 right-0 flex items-center justify-center w-12 h-12 bg-transparent border border-solid border-n-4/50 rounded-full transform-none transition-colors hover:border-n-3;
}
.splide-custom .splide__arrow:hover svg {
@apply fill-n-1;
}
.splide-custom .splide__arrow svg {
@apply w-4 h-4 fill-n-4 transform-none transition-colors;
}
.splide-visible .splide__track {
@apply overflow-visible;
}
.splide-pricing .splide__list {
@apply lg:grid !important;
@apply lg:grid-cols-3 lg:gap-4;
}
.splide-benefits .splide__list {
@apply md:grid !important;
@apply md:grid-cols-3 md:gap-x-10 md:gap-y-[4.5rem] xl:gap-y-[6rem];
}
/* 自定义工具类 */
@layer utilities {
/* 毛玻璃效果 */
.backdrop-blur-xl {
backdrop-filter: blur(24px);
}
}

View File

@@ -12,13 +12,16 @@
[class*="bytedesk"],
[id*="bytedesk"],
[class*="BytedeskWeb"] {
position: fixed !important;
z-index: 999999 !important;
pointer-events: auto !important;
}
/* Bytedesk iframe - 聊天窗口 */
iframe[src*="bytedesk"],
iframe[src*="/chat/"],
iframe[src*="/visitor/"] {
position: fixed !important;
z-index: 999999 !important;
}

89
src/styles/select-fix.css Normal file
View File

@@ -0,0 +1,89 @@
/**
* 修复 Chakra UI Select 组件的下拉选项颜色问题
* 黑金主题下,下拉选项需要深色背景和白色文字
*/
/* 所有 select 元素的 option 样式 */
select option {
background-color: #1A1A1A !important; /* 深色背景 */
color: #FFFFFF !important; /* 白色文字 */
padding: 8px !important;
}
/* 选中的 option */
select option:checked {
background-color: #2A2A2A !important;
color: #FFC107 !important; /* 金色高亮 */
}
/* hover 状态的 option (某些浏览器支持) */
select option:hover {
background-color: #222222 !important;
color: #FFD700 !important;
}
/* 禁用的 option */
select option:disabled {
color: #808080 !important;
background-color: #151515 !important;
}
/* Firefox 特殊处理 */
@-moz-document url-prefix() {
select option {
background-color: #1A1A1A !important;
color: #FFFFFF !important;
}
}
/* Webkit/Chrome 特殊处理 */
select {
/* 自定义下拉箭头颜色 */
color-scheme: dark;
}
/* 修复 Chakra UI Select 组件的特定样式 */
.chakra-select {
background-color: #1A1A1A !important;
color: #FFFFFF !important;
border-color: #333333 !important;
}
.chakra-select:hover {
border-color: #404040 !important;
}
.chakra-select:focus {
border-color: #FFC107 !important;
box-shadow: 0 0 0 1px rgba(255, 193, 7, 0.3) !important;
}
/* 下拉箭头图标 */
.chakra-select__icon-wrapper {
color: #FFFFFF !important;
}
/* 修复所有表单 select 元素 */
select[class*="chakra-select"],
select[class*="select"] {
background-color: #1A1A1A !important;
color: #FFFFFF !important;
}
/* 自定义滚动条 (适用于下拉列表) */
select::-webkit-scrollbar {
width: 8px;
}
select::-webkit-scrollbar-track {
background: #0A0A0A;
}
select::-webkit-scrollbar-thumb {
background: #333333;
border-radius: 4px;
}
select::-webkit-scrollbar-thumb:hover {
background: #FFC107;
}

View File

@@ -1,83 +0,0 @@
import { useRef, useState } from "react";
import Link from "next/link";
import { Splide, SplideTrack, SplideSlide } from "@splidejs/react-splide";
import Section from "@/components/Section";
import Image from "@/components/Image";
import { benefits } from "@/mocks/benefits";
type BenefitsProps = {};
const Benefits = ({}: BenefitsProps) => {
const [activeIndex, setActiveIndex] = useState<number>(0);
const ref = useRef<any>(null);
const handleClick = (index: number) => {
setActiveIndex(index);
ref.current?.go(index);
};
return (
<Section className="overflow-hidden">
<div className="container relative z-2">
<Splide
className="splide-benefits splide-visible max-w-[16rem] md:max-w-none"
options={{
mediaQuery: "min",
pagination: false,
arrows: false,
gap: "1.5rem",
breakpoints: {
768: {
destroy: true,
},
},
}}
onMoved={(e, newIndex) => setActiveIndex(newIndex)}
hasTrack={false}
ref={ref}
>
<SplideTrack>
{benefits.map((item) => (
<SplideSlide key={item.id}>
<div className="flex items-center mb-6">
<Image
src={item.iconUrl}
width={48}
height={48}
alt={item.title}
/>
</div>
<h5 className="h6 mb-4">{item.title}</h5>
<p className="body-2 text-n-3">{item.text}</p>
</SplideSlide>
))}
</SplideTrack>
</Splide>
<div className="flex mt-12 -mx-2 md:hidden">
{benefits.map((item, index) => (
<button
className="relative w-6 h-6 mx-2"
onClick={() => handleClick(index)}
key={item.id}
>
<span
className={`absolute inset-0 bg-conic-gradient rounded-full transition-opacity ${
index === activeIndex
? "opacity-100"
: "opacity-0"
}`}
></span>
<span className="absolute inset-0.25 bg-n-8 rounded-full">
<span className="absolute inset-2 bg-n-1 rounded-full"></span>
</span>
</button>
))}
</div>
</div>
</Section>
);
};
export default Benefits;

View File

@@ -1,83 +0,0 @@
import { useRef, useState } from "react";
import Link from "next/link";
import { Splide, SplideTrack, SplideSlide } from "@splidejs/react-splide";
import Section from "@/components/Section";
import Image from "@/components/Image";
import { benefits } from "@/mocks/benefits";
type BenefitsProps = {};
const Benefits = ({}: BenefitsProps) => {
const [activeIndex, setActiveIndex] = useState<number>(0);
const ref = useRef<any>(null);
const handleClick = (index: number) => {
setActiveIndex(index);
ref.current?.go(index);
};
return (
<Section className="overflow-hidden">
<div className="container relative z-2">
<Splide
className="splide-benefits splide-visible max-w-[16rem] md:max-w-none"
options={{
mediaQuery: "min",
pagination: false,
arrows: false,
gap: "1.5rem",
breakpoints: {
768: {
destroy: true,
},
},
}}
onMoved={(e, newIndex) => setActiveIndex(newIndex)}
hasTrack={false}
ref={ref}
>
<SplideTrack>
{benefits.map((item) => (
<SplideSlide key={item.id}>
<div className="flex items-center mb-6">
<Image
src={item.iconUrl}
width={48}
height={48}
alt={item.title}
/>
</div>
<h5 className="h6 mb-4">{item.title}</h5>
<p className="body-2 text-n-3">{item.text}</p>
</SplideSlide>
))}
</SplideTrack>
</Splide>
<div className="flex mt-12 -mx-2 md:hidden">
{benefits.map((item, index) => (
<button
className="relative w-6 h-6 mx-2"
onClick={() => handleClick(index)}
key={item.id}
>
<span
className={`absolute inset-0 bg-conic-gradient rounded-full transition-opacity ${
index === activeIndex
? "opacity-100"
: "opacity-0"
}`}
></span>
<span className="absolute inset-0.25 bg-n-8 rounded-full">
<span className="absolute inset-2 bg-n-1 rounded-full"></span>
</span>
</button>
))}
</div>
</div>
</Section>
);
};
export default Benefits;

View File

@@ -1,107 +0,0 @@
import { useRef, useState } from "react";
import { Splide, SplideSlide } from "@splidejs/react-splide";
import Section from "@/components/Section";
import Image from "@/components/Image";
import { community } from "@/mocks/community";
type CommunityProps = {};
const Community = ({}: CommunityProps) => {
const [activeIndex, setActiveIndex] = useState<number>(0);
const ref = useRef<any>(null);
const handleClick = (index: number) => {
setActiveIndex(index);
ref.current?.go(index);
};
return (
<Section className="">
<div className="container">
<div className="relative p-0.5 bg-gradient-to-b from-color-2/80 from-[4.5rem] via-color-1/40 via-[9rem] to-n-1/15 rounded-3xl">
<div className="pt-20 px-5 py-10 bg-n-8 rounded-[1.375rem] md:pt-20 md:px-20 mb:pb-16 lg:py-28 lg:pr-48">
<Splide
options={{
type: "fade",
pagination: false,
arrows: false,
}}
onMoved={(e, newIndex) => setActiveIndex(newIndex)}
ref={ref}
>
{community.map((comment) => (
<SplideSlide className="flex" key={comment.id}>
<div className="flex flex-col lg:flex-row lg:items-start">
<div className="quote mb-6 md:mb-12 lg:mb-0 lg:text-[1.75rem] lg:leading-[2.25rem]">
{comment.text}
</div>
<div className="flex items-center mt-auto lg:block lg:mt-0 lg:ml-20">
<div className="w-20 mr-6 lg:w-40 lg:mr-0 lg:mb-11">
<Image
className="w-full rounded-2xl"
src={comment.avatarUrl}
width={160}
height={160}
alt={comment.name}
/>
</div>
<div>
<h6 className="h6">
{comment.name}
</h6>
<div className="caption text-n-1/25">
{comment.role}
</div>
</div>
</div>
</div>
</SplideSlide>
))}
</Splide>
<div
className="flex justify-center mt-10 -mx-2 md:mt-12 md:justify-start lg:absolute lg:top-0
lg:right-20 lg:h-full lg:flex-col lg:justify-center lg:m-0"
>
{community.map((item: any, index: number) => (
<button
className="relative w-6 h-6 mx-2 lg:my-2 lg:mx-0"
onClick={() => handleClick(index)}
key={item.id}
>
<span
className={`absolute inset-0 bg-conic-gradient rounded-full transition-opacity ${
index === activeIndex
? "opacity-100"
: "opacity-0"
}`}
></span>
<span className="absolute inset-0.25 bg-n-8 rounded-full">
<span className="absolute inset-2 bg-n-1 rounded-full"></span>
</span>
</button>
))}
</div>
</div>
<div className="absolute -top-14 left-0 z-2 font-code text-[11.25rem] text-color-1 leading-none md:left-12">
</div>
<div className="absolute top-0 right-0 bg-n-8">
<svg width="72" height="72" viewBox="0 0 72 72">
<path
fill="#0E0C15"
stroke="#FFC876"
strokeWidth="2"
strokeOpacity=".8"
d="M-1176,1 L6.15,1 C13.89,1 21.35,3.89547 27.06,9.11714 L60.91,40.0541 C67.34,45.9271 71,54.2315 71,62.937 L71,444 C71,461.121 57.12,475 40,475 L-1176,475 C-1193.1209,475 -1207,461.121 -1207,444 L-1207,32 C-1207,14.8792 -1193.1208,1 -1176,1 Z"
/>
</svg>
</div>
</div>
</div>
</Section>
);
};
export default Community;

View File

@@ -1,107 +0,0 @@
import { useRef, useState } from "react";
import { Splide, SplideSlide } from "@splidejs/react-splide";
import Section from "@/components/Section";
import Image from "@/components/Image";
import { community } from "@/mocks/community";
type CommunityProps = {};
const Community = ({}: CommunityProps) => {
const [activeIndex, setActiveIndex] = useState<number>(0);
const ref = useRef<any>(null);
const handleClick = (index: number) => {
setActiveIndex(index);
ref.current?.go(index);
};
return (
<Section className="">
<div className="container">
<div className="relative p-0.5 bg-gradient-to-b from-color-2/80 from-[4.5rem] via-color-1/40 via-[9rem] to-n-1/15 rounded-3xl">
<div className="pt-20 px-5 py-10 bg-n-8 rounded-[1.375rem] md:pt-20 md:px-20 mb:pb-16 lg:py-28 lg:pr-48">
<Splide
options={{
type: "fade",
pagination: false,
arrows: false,
}}
onMoved={(e, newIndex) => setActiveIndex(newIndex)}
ref={ref}
>
{community.map((comment) => (
<SplideSlide className="flex" key={comment.id}>
<div className="flex flex-col lg:flex-row lg:items-start">
<div className="quote mb-6 md:mb-12 lg:mb-0 lg:text-[1.75rem] lg:leading-[2.25rem]">
{comment.text}
</div>
<div className="flex items-center mt-auto lg:block lg:mt-0 lg:ml-20">
<div className="w-20 mr-6 lg:w-40 lg:mr-0 lg:mb-11">
<Image
className="w-full rounded-2xl"
src={comment.avatarUrl}
width={160}
height={160}
alt={comment.name}
/>
</div>
<div>
<h6 className="h6">
{comment.name}
</h6>
<div className="caption text-n-1/25">
{comment.role}
</div>
</div>
</div>
</div>
</SplideSlide>
))}
</Splide>
<div
className="flex justify-center mt-10 -mx-2 md:mt-12 md:justify-start lg:absolute lg:top-0
lg:right-20 lg:h-full lg:flex-col lg:justify-center lg:m-0"
>
{community.map((item: any, index: number) => (
<button
className="relative w-6 h-6 mx-2 lg:my-2 lg:mx-0"
onClick={() => handleClick(index)}
key={item.id}
>
<span
className={`absolute inset-0 bg-conic-gradient rounded-full transition-opacity ${
index === activeIndex
? "opacity-100"
: "opacity-0"
}`}
></span>
<span className="absolute inset-0.25 bg-n-8 rounded-full">
<span className="absolute inset-2 bg-n-1 rounded-full"></span>
</span>
</button>
))}
</div>
</div>
<div className="absolute -top-14 left-0 z-2 font-code text-[11.25rem] text-color-1 leading-none md:left-12">
</div>
<div className="absolute top-0 right-0 bg-n-8">
<svg width="72" height="72" viewBox="0 0 72 72">
<path
fill="#0E0C15"
stroke="#FFC876"
strokeWidth="2"
strokeOpacity=".8"
d="M-1176,1 L6.15,1 C13.89,1 21.35,3.89547 27.06,9.11714 L60.91,40.0541 C67.34,45.9271 71,54.2315 71,62.937 L71,444 C71,461.121 57.12,475 40,475 L-1176,475 C-1193.1209,475 -1207,461.121 -1207,444 L-1207,32 C-1207,14.8792 -1193.1208,1 -1176,1 Z"
/>
</svg>
</div>
</div>
</div>
</Section>
);
};
export default Community;

View File

@@ -1,101 +0,0 @@
import Section from "@/components/Section";
import Image from "@/components/Image";
type FeaturesProps = {};
const Features = ({}: FeaturesProps) => {
const content = [
{
id: "0",
title: "Seamless Integration",
text: "With smart automation and top-notch security, it's the perfect solution for teams looking to work smarter.",
},
{
id: "1",
title: "Smart Automation",
},
{
id: "2",
title: "Top-notch Security",
},
];
return (
<Section>
<div className="container">
<div className="-mb-16">
{[
{ id: "0", imageUrl: "/images/features/image-1.jpg" },
{ id: "1", imageUrl: "/images/features/image-1.jpg" },
{ id: "2", imageUrl: "/images/features/image-1.jpg" },
].map((item, index) => (
<div
className="mb-16 md:grid md:grid-cols-2 md:items-center lg:gap-20 xl:gap-40"
key={item.id}
>
<div
className={`mb-8 bg-n-6 rounded-3xl md:relative md:mb-0 ${
index % 2 === 0 ? "" : "md:order-1"
}`}
>
<Image
className="w-full rounded-3xl"
src={item.imageUrl}
width={550}
height={600}
alt="Image"
/>
<div
className={`hidden absolute top-5 -right-8 bottom-5 grid-cols-2 w-8 md:grid ${
index % 2 === 0
? "-right-8"
: "-left-8 rotate-180"
}`}
>
<div className="rounded-r-[1.25rem] bg-[#1B1B2E]"></div>
<div className="my-5 rounded-r-[1.25rem] bg-[#1B1B2E]/50"></div>
</div>
</div>
<div
className={
index % 2 === 0 ? "md:pl-16" : "md:pr-16"
}
>
<h2 className="h2 mb-4 md:mb-8">
Customization Options
</h2>
<ul className="">
{content.map((item) => (
<li
className="py-4 border-b border-n-1/5 md:py-6"
key={item.id}
>
<div className="flex items-center">
<Image
src="/images/check.svg"
width={24}
height={24}
alt="Check"
/>
<h6 className="body-2 ml-5">
{item.title}
</h6>
</div>
{item.text && (
<p className="body-2 mt-3 text-n-4">
{item.text}
</p>
)}
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
</Section>
);
};
export default Features;

View File

@@ -1,101 +0,0 @@
import Section from "@/components/Section";
import Image from "@/components/Image";
type FeaturesProps = {};
const Features = ({}: FeaturesProps) => {
const content = [
{
id: "0",
title: "Seamless Integration",
text: "With smart automation and top-notch security, it's the perfect solution for teams looking to work smarter.",
},
{
id: "1",
title: "Smart Automation",
},
{
id: "2",
title: "Top-notch Security",
},
];
return (
<Section>
<div className="container">
<div className="-mb-16">
{[
{ id: "0", imageUrl: "/images/features/image-1.jpg" },
{ id: "1", imageUrl: "/images/features/image-1.jpg" },
{ id: "2", imageUrl: "/images/features/image-1.jpg" },
].map((item, index) => (
<div
className="mb-16 md:grid md:grid-cols-2 md:items-center lg:gap-20 xl:gap-40"
key={item.id}
>
<div
className={`mb-8 bg-n-6 rounded-3xl md:relative md:mb-0 ${
index % 2 === 0 ? "" : "md:order-1"
}`}
>
<Image
className="w-full rounded-3xl"
src={item.imageUrl}
width={550}
height={600}
alt="Image"
/>
<div
className={`hidden absolute top-5 -right-8 bottom-5 grid-cols-2 w-8 md:grid ${
index % 2 === 0
? "-right-8"
: "-left-8 rotate-180"
}`}
>
<div className="rounded-r-[1.25rem] bg-[#1B1B2E]"></div>
<div className="my-5 rounded-r-[1.25rem] bg-[#1B1B2E]/50"></div>
</div>
</div>
<div
className={
index % 2 === 0 ? "md:pl-16" : "md:pr-16"
}
>
<h2 className="h2 mb-4 md:mb-8">
Customization Options
</h2>
<ul className="">
{content.map((item) => (
<li
className="py-4 border-b border-n-1/5 md:py-6"
key={item.id}
>
<div className="flex items-center">
<Image
src="/images/check.svg"
width={24}
height={24}
alt="Check"
/>
<h6 className="body-2 ml-5">
{item.title}
</h6>
</div>
{item.text && (
<p className="body-2 mt-3 text-n-4">
{item.text}
</p>
)}
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
</Section>
);
};
export default Features;

View File

@@ -1,38 +0,0 @@
import Heading from "@/components/Heading";
import Image from "@/components/Image";
import Section from "@/components/Section";
type HeroProps = {};
const Hero = ({}: HeroProps) => (
<Section className="overflow-hidden md:-mb-10 xl:-mb-20">
<div className="container relative z-2 md:grid md:grid-cols-2 md:items-center md:gap-10 lg:gap-48">
<Heading
className="md:mt-12 lg:max-w-[30rem] lg:mt-20"
textAlignClassName="md:text-left"
titleLarge="Main features of Brainwave"
textLarge="Here are some of the core features of Brainwavethat make it stand out from other chat applications"
/>
<div className="relative">
<Image
className="w-full md:min-w-[125%] xl:min-w-full"
src="/images/features/features.png"
width={547}
height={588}
alt="Features"
/>
<div className="absolute top-0 left-1/2 w-full">
<Image
className="w-full"
src="/images/grid.png"
width={550}
height={550}
alt="Grid"
/>
</div>
</div>
</div>
</Section>
);
export default Hero;

View File

@@ -1,38 +0,0 @@
import Heading from "@/components/Heading";
import Image from "@/components/Image";
import Section from "@/components/Section";
type HeroProps = {};
const Hero = ({}: HeroProps) => (
<Section className="overflow-hidden md:-mb-10 xl:-mb-20">
<div className="container relative z-2 md:grid md:grid-cols-2 md:items-center md:gap-10 lg:gap-48">
<Heading
className="md:mt-12 lg:max-w-[30rem] lg:mt-20"
textAlignClassName="md:text-left"
titleLarge="Main features of Brainwave"
textLarge="Here are some of the core features of Brainwavethat make it stand out from other chat applications"
/>
<div className="relative">
<Image
className="w-full md:min-w-[125%] xl:min-w-full"
src="/images/features/features.png"
width={547}
height={588}
alt="Features"
/>
<div className="absolute top-0 left-1/2 w-full">
<Image
className="w-full"
src="/images/grid.png"
width={550}
height={550}
alt="Grid"
/>
</div>
</div>
</div>
</Section>
);
export default Hero;

View File

@@ -1,24 +0,0 @@
"use client";
import Layout from "@/components/Layout";
import Services from "@/components/Services";
import Join from "@/components/Join";
import Hero from "./Hero";
import Benefits from "./Benefits";
import Features from "./Features";
import Community from "./Community";
const FeaturesPage = () => {
return (
<Layout>
<Hero />
<Benefits />
<Features />
<Community />
<Services containerClassName="md:pb-10" />
<Join />
</Layout>
);
};
export default FeaturesPage;

Some files were not shown because too many files have changed in this diff Show More