Compare commits

..

70 Commits

Author SHA1 Message Date
zdl
cf4fdf6a68 feat: 传导练UI调整 2025-11-28 07:14:52 +08:00
zdl
34338373cd fix: UI调试 2025-11-27 18:27:44 +08:00
zdl
589e1c20f9 fix: 调整相关概念卡片UI 2025-11-27 17:22:49 +08:00
zdl
60e9a40a1f fix: 文案调整 2025-11-27 17:03:35 +08:00
zdl
b8b24643fe fix: AI合成h5换行,pc一行,评论标题上方margin去掉 2025-11-27 16:55:25 +08:00
zdl
e9e9ec9051 fix: 调整AI合成UI 2025-11-27 16:40:35 +08:00
zdl
5b0e420770 fix: 分时图UI调整 2025-11-27 16:20:15 +08:00
zdl
93f43054fd fix:事件详情弹窗UI 2025-11-27 15:35:48 +08:00
zdl
101d042b0e fix:调整客服UI 2025-11-27 15:31:07 +08:00
zdl
a1aa6718e6 fix: 事件详情弹窗UI调整 2025-11-27 15:08:14 +08:00
zdl
753727c1c0 fix: 事件详情弹窗UI调整
重要性h5不展示
事件列表卡片间距调整
2025-11-27 14:40:38 +08:00
zdl
afc92ee583 fix: h5 去掉通知弹窗引导 2025-11-27 13:37:01 +08:00
zdl
d825e4fe59 fix: 关注按钮UI调整 2025-11-27 11:19:20 +08:00
zdl
62cf0a6c7d feat: 修改小程序跳转链接 2025-11-27 10:46:14 +08:00
zdl
805d446775 feat: 调整搜索框UI 2025-11-26 19:33:00 +08:00
zdl
24ddfcd4b5 feat: 新增:H5 时左右 padding 改为 8px 2025-11-26 19:31:12 +08:00
zdl
a90158239b feat: 模式切花移动到标题恻,通知UI调整 2025-11-26 19:11:33 +08:00
zdl
a8d4245595 pref: 文案调整 2025-11-26 17:49:39 +08:00
zdl
5aedde7528 feat:H5 移动端已隐藏整个顶部控制栏 2025-11-26 16:51:52 +08:00
zdl
f5f89a1c72 feat:箭头绝对定位
移除左右 padding
隐藏重复箭头
2025-11-26 16:50:46 +08:00
zdl
e0b7f8c59d feat: 调整事件列表h5模式调整 2025-11-26 16:44:53 +08:00
zdl
d22d75e761 pref: h5 分页UI调整 2025-11-26 16:35:49 +08:00
zdl
30fc156474 fix: 移动端事件中心事件列表添加时间 2025-11-26 16:23:28 +08:00
zdl
572665199a feat: 删除事件中心页面不再显示桌面通知提示横幅 2025-11-26 16:18:15 +08:00
zdl
a2831c82a8 feat: 移动端不显示政策标签 2025-11-26 16:02:59 +08:00
zdl
217551b6ab feat: H5 移动端将隐藏"开启通知"组件,桌面端保持正常显示 2025-11-26 16:01:58 +08:00
zdl
022271947a fix: 移动端抽屉菜单不再显示深色模式切换按钮 2025-11-26 15:47:26 +08:00
zdl
cd6ffdbe68 fix: 修复hooks报错 2025-11-26 15:45:46 +08:00
zdl
9df725b748 feat: 精简日志 2025-11-26 15:34:11 +08:00
zdl
64f8914951 feat: logger.js - 添加日志级别控制 2025-11-26 15:30:31 +08:00
zdl
506e5a448c feat: 本地优先启动服务拦截 2025-11-26 15:23:37 +08:00
zdl
e277352133 src/contexts/NotificationContext.js
- 添加 selectIsMobile 导入 在 NotificationProvider 组件开头添加移动端检测 移动端返回空壳 Provider
  - 桌面端保持原有完整功能
  移除 ConnectionStatusBar 组件和 ConnectionStatusBarWrapper(所有端)
  - 移除了不再使用的 useNotification、useLocation、logger 导入
  - 添加了 Redux selectIsMobile 检测
  - 移动端不渲染 NotificationContainer
2025-11-26 15:15:20 +08:00
zdl
87437ed229 feat: 增加 wechat_login=success 参数处理 2025-11-26 14:52:49 +08:00
zdl
037471d880 feat: 修复 Mock 路径从 h5-auth-url → h5-auth 2025-11-26 14:52:05 +08:00
zdl
0c482bc72c feat: 回调处理增加 H5 模式判断,重定向到前端回调页 2025-11-26 14:51:51 +08:00
zdl
4aebb3bf4b feat: 调整导航栏高度 2025-11-26 14:10:09 +08:00
zdl
ed241bd9c5 pref: 导航选中高亮 2025-11-26 14:01:58 +08:00
zdl
e6ede81c78 feat: 修复动态 reducer 注入导致的运行时错误 2025-11-26 13:59:26 +08:00
zdl
a0b688da80 feat: 移除 PerformanceMonitor 调试日志 2025-11-26 13:42:42 +08:00
zdl
6bd09b797d feat: PostHog 加载策略优化计划
目标

     改进 PostHog 延迟加载策略,平衡首屏性能和数据完整性:
     1. 使用 requestIdleCallback 替代固定 2 秒延迟
     2. 保留关键事件(first_visit)的同步追踪,确保数据不丢失
2025-11-26 13:41:09 +08:00
zdl
9c532b5f18 pref: 删除微信登陆日志 2025-11-26 13:38:26 +08:00
zdl
1d1d6c8169 pref: P0: PostHog 延迟加载 - 完成
P0: HeroPanel 懒加载 -  完成
 P0/P1: Charts/FullCalendar 懒加载 -  已通过路由懒加载隔离,无需额外处理
删除空的 CSS 文件
2025-11-26 13:33:58 +08:00
zdl
3507cfe9f7 pref: 删除调试工具 2025-11-26 13:16:30 +08:00
zdl
cc520893f8 fix: 添加 wechatStatusRef 用于跟踪最新状态
使用 wechatStatusRef.current 替代 wechatStatus
添加 AUTHORIZED 状态处理逻辑
添加 useEffect 同步 wechatStatusRef
2025-11-26 13:07:46 +08:00
zdl
dabedc1c0b feat: 之前的防重复逻辑 !subscriptionInfo.type 永远为 false(因为初始值是 free),导致订阅 API 从不被调用 2025-11-26 11:49:12 +08:00
zdl
7b4c4be7bf pref:点击手机登陆后日志优化 2025-11-26 11:43:16 +08:00
zdl
7a2c73f3ca :pref: 首屏优化 2025-11-26 11:30:12 +08:00
zdl
105a0b02ea fix:移除日志 2025-11-26 11:17:03 +08:00
zdl
d8a4c20565 fix: Login Page Viewed 事件仅在模态框首次打开时触发 1 次
- 验证码倒计时期间不再重复触发
     - 不影响其他事件追踪功能\
2025-11-26 11:15:04 +08:00
zdl
5f959fb44f feat: 添加 认证检查 和 首页渲染 的性能标记 2025-11-26 11:10:50 +08:00
zdl
ee78e00d3b feat: 添加设备检测功能 2025-11-26 11:03:13 +08:00
zdl
2fcc341213 feat: 修复通知初始化问题 2025-11-26 10:55:38 +08:00
zdl
1090a2fc67 feat: 客服接口mock添加 2025-11-26 10:55:18 +08:00
zdl
77f3949fe2 feat: 首页添加性能监控 2025-11-26 10:54:57 +08:00
zdl
742ab337dc feat: 更新测试用例 2025-11-26 10:54:20 +08:00
zdl
d2b6904a4a pref: 删除无用组件 2025-11-26 10:40:14 +08:00
zdl
789a6229a7 feat: 添加设备类型 2025-11-26 10:39:33 +08:00
zdl
6886a649f5 perf: Socket 连接异步化,使用 requestIdleCallback 不阻塞首屏
- NotificationContext: 将 Socket 初始化包裹在 requestIdleCallback 中
- 设置 3 秒超时保护,确保连接不会被无限延迟
- 不支持 requestIdleCallback 的浏览器自动降级到 setTimeout(0)
- socket/index.js: 移除模块加载时的 console.log,减少首屏阻塞感知
- 所有页面的首屏渲染都不再被 Socket 连接阻塞

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:34:45 +08:00
zdl
581e874b0d fix:修复类型提示错误 2025-11-26 10:11:02 +08:00
zdl
b23ed93020 Merge branch 'feature_2025/251117_pref' into feature_2025/251121_h5UI
* feature_2025/251117_pref:
  update pay function
  update pay function
  update pay function
2025-11-26 09:59:01 +08:00
zdl
84f70f3329 pref: 权限校验中 - 显示占位骨架,不显示登录按钮或用户菜单,/home页面添加骨架屏逻辑 2025-11-26 09:57:20 +08:00
zdl
601b06d79e fix: 修复 AgentChat hooks 中的 logger 调用 2025-11-26 09:47:54 +08:00
zdl
0818a7bff7 fix: 修复 logger 函数签名问题 2025-11-26 09:44:21 +08:00
ce19881181 update pay function 2025-11-26 09:06:02 +08:00
bef3e86f60 update pay function 2025-11-26 09:00:38 +08:00
65deea43e2 update pay function 2025-11-26 08:44:22 +08:00
c7a881c965 update pay function 2025-11-25 20:22:45 +08:00
6932796b00 update pay function 2025-11-25 19:38:50 +08:00
zdl
03f1331202 feat: homets 化 创建类型定义文件
创建常量配置文件
 创建自定义 Hook

 创建组件目录
创建 HeroHeader 组件
创建 FeaturedFeatureCard 组件
创建 FeatureCard 组件
创建新的 HomePage.tsx
2025-11-25 17:58:53 +08:00
zdl
c771f7cae6 feat: 首页删除ME-Agent 实时分析系统 2025-11-25 17:58:36 +08:00
113 changed files with 7356 additions and 4531 deletions

221
app.py
View File

@@ -26,6 +26,7 @@ import re
import string
from datetime import datetime, timedelta, time as dt_time, date
from clickhouse_driver import Client as Cclient
from elasticsearch import Elasticsearch
from flask_cors import CORS
from collections import defaultdict
@@ -138,6 +139,15 @@ engine_2 = create_engine(
pool_timeout=30,
max_overflow=10
)
# Elasticsearch 客户端初始化
es_client = Elasticsearch(
hosts=["http://222.128.1.157:19200"],
request_timeout=30,
max_retries=3,
retry_on_timeout=True
)
app = Flask(__name__)
# 存储验证码的临时字典生产环境应使用Redis
verification_codes = {}
@@ -3465,6 +3475,46 @@ def get_wechat_qrcode():
}}), 200
@app.route('/api/auth/wechat/h5-auth', methods=['POST'])
def get_wechat_h5_auth_url():
"""
获取微信 H5 网页授权 URL
用于手机浏览器跳转微信 App 授权
"""
data = request.get_json() or {}
frontend_redirect = data.get('redirect_url', '/home')
# 生成唯一 state
state = uuid.uuid4().hex
# 编码回调地址
redirect_uri = urllib.parse.quote_plus(WECHAT_REDIRECT_URI)
# 构建授权 URL使用 snsapi_userinfo 获取用户信息,仅限微信内 H5 使用)
auth_url = (
f"https://open.weixin.qq.com/connect/oauth2/authorize?"
f"appid={WECHAT_APPID}&redirect_uri={redirect_uri}"
f"&response_type=code&scope=snsapi_userinfo&state={state}"
"#wechat_redirect"
)
# 存储 session 信息
wechat_qr_sessions[state] = {
'status': 'waiting',
'expires': time.time() + 300,
'mode': 'h5', # 标记为 H5 模式
'frontend_redirect': frontend_redirect,
'user_info': None,
'wechat_openid': None,
'wechat_unionid': None
}
return jsonify({
'auth_url': auth_url,
'state': state
}), 200
@app.route('/api/account/wechat/qrcode', methods=['GET'])
def get_wechat_bind_qrcode():
"""发起微信绑定二维码,会话标记为绑定模式"""
@@ -3704,14 +3754,23 @@ def wechat_callback():
# 更新微信session状态供前端轮询检测
if state in wechat_qr_sessions:
session_item = wechat_qr_sessions[state]
# 仅处理登录/注册流程,不处理绑定流程
if not session_item.get('mode'):
# 更新状态和用户信息
mode = session_item.get('mode')
# H5 模式:重定向到前端回调页面
if mode == 'h5':
frontend_redirect = session_item.get('frontend_redirect', '/home/wechat-callback')
# 清理 session
del wechat_qr_sessions[state]
print(f"✅ H5 微信登录成功,重定向到: {frontend_redirect}")
return redirect(f"{frontend_redirect}?wechat_login=success")
# PC 扫码模式:更新状态供前端轮询
if not mode:
session_item['status'] = 'register_ready' if is_new_user else 'login_ready'
session_item['user_info'] = {'user_id': user.id}
print(f"✅ 微信扫码状态已更新: {session_item['status']}, user_id: {user.id}")
# 直接跳转到首页
# PC 模式直接跳转到首页
return redirect('/home')
except Exception as e:
@@ -11688,108 +11747,98 @@ def get_daily_top_concepts():
@app.route('/api/market/rise-analysis/<seccode>', methods=['GET'])
def get_rise_analysis(seccode):
"""获取股票涨幅分析数据"""
"""获取股票涨幅分析数据(从 Elasticsearch 获取)"""
try:
# 获取日期范围参数
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
limit = request.args.get('limit', 100, type=int)
query = text("""
SELECT stock_code,
stock_name,
trade_date,
rise_rate,
close_price,
volume,
amount,
main_business,
rise_reason_brief,
rise_reason_detail,
news_summary,
announcements,
guba_sentiment,
analysis_time
FROM stock_rise_analysis
WHERE stock_code = :stock_code
""")
# 构建 ES 查询
must_conditions = [
{"term": {"stock_code": seccode}}
]
params = {'stock_code': seccode}
# 添加日期筛选
# 添加日期范围筛选
if start_date and end_date:
query = text("""
SELECT stock_code,
stock_name,
trade_date,
rise_rate,
close_price,
volume,
amount,
main_business,
rise_reason_brief,
rise_reason_detail,
news_summary,
announcements,
guba_sentiment,
analysis_time
FROM stock_rise_analysis
WHERE stock_code = :stock_code
AND trade_date BETWEEN :start_date AND :end_date
ORDER BY trade_date DESC
""")
params['start_date'] = start_date
params['end_date'] = end_date
else:
query = text("""
SELECT stock_code,
stock_name,
trade_date,
rise_rate,
close_price,
volume,
amount,
main_business,
rise_reason_brief,
rise_reason_detail,
news_summary,
announcements,
guba_sentiment,
analysis_time
FROM stock_rise_analysis
WHERE stock_code = :stock_code
ORDER BY trade_date DESC LIMIT 100
""")
must_conditions.append({
"range": {
"trade_date": {
"gte": start_date,
"lte": end_date,
"format": "yyyy-MM-dd"
}
}
})
with engine.connect() as conn:
result = conn.execute(query, params).fetchall()
es_query = {
"query": {
"bool": {
"must": must_conditions
}
},
"sort": [
{"trade_date": {"order": "desc"}}
],
"size": limit,
"_source": {
"excludes": ["rise_reason_detail_embedding"] # 排除向量字段
}
}
# 执行 ES 查询
response = es_client.search(index="stock_rise_analysis", body=es_query)
# 格式化数据
rise_analysis_data = []
for row in result:
for hit in response['hits']['hits']:
source = hit['_source']
# 处理研报引用数据
verification_reports = []
if source.get('has_verification_info') and source.get('verification_info'):
v_info = source['verification_info']
processed_results = v_info.get('processed_result', [])
for report in processed_results:
verification_reports.append({
'publisher': report.get('publisher', ''),
'report_title': report.get('report_title', ''),
'author': report.get('author', ''),
'declare_date': report.get('declare_date', ''),
'content': report.get('content', ''),
'verification_item': report.get('verification_item', ''),
'match_ratio': report.get('match_ratio', 0),
'match_score': report.get('match_score', '')
})
rise_analysis_data.append({
'stock_code': row.stock_code,
'stock_name': row.stock_name,
'trade_date': format_date(row.trade_date),
'rise_rate': format_decimal(row.rise_rate),
'close_price': format_decimal(row.close_price),
'volume': format_decimal(row.volume),
'amount': format_decimal(row.amount),
'main_business': row.main_business,
'rise_reason_brief': row.rise_reason_brief,
'rise_reason_detail': row.rise_reason_detail,
'news_summary': row.news_summary,
'announcements': row.announcements,
'guba_sentiment': row.guba_sentiment,
'analysis_time': row.analysis_time.strftime('%Y-%m-%d %H:%M:%S') if row.analysis_time else None
'stock_code': source.get('stock_code', ''),
'stock_name': source.get('stock_name', ''),
'trade_date': source.get('trade_date', ''),
'rise_rate': source.get('rise_rate', 0),
'close_price': source.get('close_price', 0),
'volume': source.get('volume', 0),
'amount': source.get('amount', 0),
'main_business': source.get('main_business', ''),
'rise_reason_brief': source.get('rise_reason_brief', ''),
'rise_reason_detail': source.get('rise_reason_detail', ''),
'announcements': source.get('announcements', ''),
'verification_reports': verification_reports,
'has_verification_info': source.get('has_verification_info', False),
'create_time': source.get('create_time', ''),
'update_time': source.get('update_time', '')
})
return jsonify({
'success': True,
'data': rise_analysis_data,
'count': len(rise_analysis_data)
'count': len(rise_analysis_data),
'total': response['hits']['total']['value']
})
except Exception as e:
import traceback
print(f"ES查询错误: {traceback.format_exc()}")
return jsonify({
'success': False,
'error': str(e)

332
app_vx.py
View File

@@ -55,6 +55,9 @@ from datetime import datetime, timedelta, time as dt_time
from werkzeug.security import generate_password_hash, check_password_hash
import json
from clickhouse_driver import Client as Cclient
from queue import Queue, Empty, Full
from threading import Lock, RLock
from contextlib import contextmanager
import jwt
from docx import Document
from tencentcloud.common import credential
@@ -69,6 +72,289 @@ engine_med = create_engine("mysql+pymysql://root:Zzl5588161!@222.128.1.157:33060
engine_2 = create_engine("mysql+pymysql://root:Zzl5588161!@222.128.1.157:33060/valuefrontier", echo=False)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# ===================== ClickHouse 连接池实现 =====================
class ClickHouseConnectionPool:
"""
ClickHouse 连接池
- 支持连接复用,避免频繁创建/销毁连接
- 支持连接超时和健康检查
- 支持自动重连
- 线程安全
"""
def __init__(self, host, port, user, password, database,
pool_size=10, max_overflow=10,
connection_timeout=10, query_timeout=30,
health_check_interval=60):
"""
初始化连接池
Args:
host: ClickHouse 主机地址
port: ClickHouse 端口
user: 用户名
password: 密码
database: 数据库名
pool_size: 连接池核心大小(预创建连接数)
max_overflow: 最大溢出连接数(总连接数 = pool_size + max_overflow
connection_timeout: 获取连接超时时间(秒)
query_timeout: 查询超时时间(秒)
health_check_interval: 健康检查间隔(秒)
"""
self.host = host
self.port = port
self.user = user
self.password = password
self.database = database
self.pool_size = pool_size
self.max_overflow = max_overflow
self.connection_timeout = connection_timeout
self.query_timeout = query_timeout
self.health_check_interval = health_check_interval
# 连接池队列
self._pool = Queue(maxsize=pool_size + max_overflow)
# 当前活跃连接数
self._active_connections = 0
# 锁
self._lock = RLock()
# 连接最后使用时间记录
self._last_used = {}
# 连接创建时间记录
self._created_at = {}
# 初始化核心连接
self._init_pool()
logger.info(f"ClickHouse 连接池初始化完成: pool_size={pool_size}, max_overflow={max_overflow}")
def _init_pool(self):
"""初始化连接池,预创建部分核心连接(非阻塞)"""
# 只预创建 2 个连接,其余按需创建
init_count = min(2, self.pool_size)
for i in range(init_count):
try:
conn = self._create_connection()
if conn:
self._pool.put(conn)
logger.info(f"预创建 ClickHouse 连接 {i+1}/{init_count} 成功")
except Exception as e:
logger.warning(f"预创建 ClickHouse 连接失败 ({i+1}/{init_count}): {e}")
# 预创建失败不阻塞启动,后续按需创建
break
def _create_connection(self):
"""创建新的 ClickHouse 连接"""
try:
client = Cclient(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database,
connect_timeout=self.connection_timeout,
send_receive_timeout=self.query_timeout,
sync_request_timeout=self.query_timeout,
settings={
'max_execution_time': self.query_timeout,
'connect_timeout': self.connection_timeout,
}
)
conn_id = id(client)
self._created_at[conn_id] = time.time()
self._last_used[conn_id] = time.time()
with self._lock:
self._active_connections += 1
logger.debug(f"创建新的 ClickHouse 连接: {conn_id}")
return client
except Exception as e:
logger.error(f"创建 ClickHouse 连接失败: {e}")
raise
def _check_connection_health(self, conn):
"""检查连接健康状态"""
try:
conn_id = id(conn)
last_used = self._last_used.get(conn_id, 0)
# 如果连接长时间未使用,进行健康检查
if time.time() - last_used > self.health_check_interval:
# 执行简单查询检查连接
conn.execute("SELECT 1")
logger.debug(f"连接 {conn_id} 健康检查通过")
return True
except Exception as e:
logger.warning(f"连接健康检查失败: {e}")
return False
def _close_connection(self, conn):
"""关闭连接"""
try:
conn_id = id(conn)
conn.disconnect()
self._last_used.pop(conn_id, None)
self._created_at.pop(conn_id, None)
with self._lock:
self._active_connections = max(0, self._active_connections - 1)
logger.debug(f"关闭 ClickHouse 连接: {conn_id}")
except Exception as e:
logger.warning(f"关闭连接时出错: {e}")
def get_connection(self, timeout=None):
"""
从连接池获取连接
Args:
timeout: 获取连接的超时时间,默认使用 connection_timeout
Returns:
ClickHouse 客户端连接
Raises:
TimeoutError: 获取连接超时
Exception: 创建连接失败
"""
timeout = timeout or self.connection_timeout
# 首先尝试从池中获取连接
try:
conn = self._pool.get(block=True, timeout=timeout)
# 检查连接健康状态
if self._check_connection_health(conn):
self._last_used[id(conn)] = time.time()
return conn
else:
# 连接不健康,关闭并创建新连接
self._close_connection(conn)
return self._create_connection()
except Empty:
# 池中没有可用连接,检查是否可以创建新连接
with self._lock:
if self._active_connections < self.pool_size + self.max_overflow:
try:
return self._create_connection()
except Exception as e:
logger.error(f"创建溢出连接失败: {e}")
raise
# 已达到最大连接数,等待连接释放
logger.warning(f"连接池已满,等待连接释放... (当前连接数: {self._active_connections})")
raise TimeoutError(f"获取 ClickHouse 连接超时 (timeout={timeout}s)")
def release_connection(self, conn):
"""
释放连接回连接池
Args:
conn: 要释放的连接
"""
if conn is None:
return
conn_id = id(conn)
self._last_used[conn_id] = time.time()
try:
self._pool.put(conn, block=False)
logger.debug(f"连接 {conn_id} 已释放回连接池")
except Full:
# 池已满,关闭多余连接
logger.debug(f"连接池已满,关闭多余连接: {conn_id}")
self._close_connection(conn)
@contextmanager
def connection(self, timeout=None):
"""
上下文管理器方式获取连接
Usage:
with pool.connection() as conn:
result = conn.execute("SELECT * FROM table")
"""
conn = None
try:
conn = self.get_connection(timeout)
yield conn
except Exception as e:
# 发生异常时,检查连接是否需要重建
if conn:
try:
# 尝试简单查询检测连接状态
conn.execute("SELECT 1")
except:
# 连接已损坏,关闭它
self._close_connection(conn)
conn = None
raise
finally:
if conn:
self.release_connection(conn)
def execute(self, query, params=None, timeout=None):
"""
执行查询(自动管理连接)
Args:
query: SQL 查询语句
params: 查询参数
timeout: 查询超时时间
Returns:
查询结果
"""
with self.connection(timeout) as conn:
return conn.execute(query, params)
def get_pool_status(self):
"""获取连接池状态"""
return {
'pool_size': self.pool_size,
'max_overflow': self.max_overflow,
'active_connections': self._active_connections,
'available_connections': self._pool.qsize(),
'max_connections': self.pool_size + self.max_overflow
}
def close_all(self):
"""关闭所有连接"""
while not self._pool.empty():
try:
conn = self._pool.get_nowait()
self._close_connection(conn)
except Empty:
break
logger.info("ClickHouse 连接池已关闭所有连接")
# 初始化全局 ClickHouse 连接池(懒加载模式)
clickhouse_pool = None
_pool_lock = Lock()
def _init_clickhouse_pool():
"""懒加载初始化 ClickHouse 连接池"""
global clickhouse_pool
if clickhouse_pool is None:
with _pool_lock:
if clickhouse_pool is None:
clickhouse_pool = ClickHouseConnectionPool(
host='222.128.1.157',
port=18000,
user='default',
password='Zzl33818!',
database='stock',
pool_size=5, # 减少预创建连接数
max_overflow=20, # 增加溢出连接数,总共支持 25 并发
connection_timeout=10, # 连接超时 10 秒
query_timeout=30, # 查询超时 30 秒
health_check_interval=60 # 60 秒未使用的连接进行健康检查
)
return clickhouse_pool
# ===================== ClickHouse 连接池实现结束 =====================
app = Flask(__name__)
Compress(app)
UPLOAD_FOLDER = 'static/uploads/avatars'
@@ -1150,13 +1436,40 @@ def update_investment_preferences():
def get_clickhouse_client():
return Cclient(
host='222.128.1.157',
port=18000,
user='default',
password='Zzl33818!',
database='stock'
)
"""
获取 ClickHouse 客户端(使用连接池,懒加载)
返回连接池对象,支持两种使用方式:
方式1推荐- 直接调用 execute
client = get_clickhouse_client()
result = client.execute("SELECT * FROM table", {'param': value})
方式2 - 使用上下文管理器:
client = get_clickhouse_client()
with client.connection() as conn:
result = conn.execute("SELECT * FROM table")
"""
return _init_clickhouse_pool()
@app.route('/api/system/clickhouse-pool-status', methods=['GET'])
def api_clickhouse_pool_status():
"""获取 ClickHouse 连接池状态(仅供监控使用)"""
try:
pool = _init_clickhouse_pool()
status = pool.get_pool_status()
return jsonify({
'code': 200,
'message': 'success',
'data': status
})
except Exception as e:
return jsonify({
'code': 500,
'message': str(e),
'data': None
}), 500
@app.route('/api/stock/<stock_code>/kline')
@@ -1292,6 +1605,7 @@ def get_daily_kline(stock_code, event_datetime, stock_name):
'name': stock_name,
'data': kline_data,
'trade_date': event_datetime.date().strftime('%Y-%m-%d'),
'event_time': event_datetime.isoformat(),
'type': 'daily',
'is_history': True,
'data_count': len(kline_data)
@@ -4120,7 +4434,9 @@ def api_stock_detail(event_id, stock_code):
'event_info': {
'event_id': event.id,
'event_title': event.title,
'event_description': event.description
'event_description': event.description,
'event_start_time': event.start_time.isoformat() if event.start_time else None,
'event_created_at': event.created_at.strftime("%Y-%m-%d %H:%M:%S") if event.created_at else None
},
'basic_info': {
'stock_code': basic_info.SECCODE,

553
public/htmls/TPU芯片.html Normal file
View File

@@ -0,0 +1,553 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TPU芯片 - 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://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<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&display=swap');
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.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;
}
.timeline-dot {
position: relative;
z-index: 1;
}
.timeline-line {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(to bottom, transparent, #667eea, #764ba2, transparent);
transform: translateX(-50%);
}
.stat-card {
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.table-responsive {
overflow-x: auto;
}
.table-responsive::-webkit-scrollbar {
height: 8px;
}
.table-responsive::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.table-responsive::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.table-responsive::-webkit-scrollbar-thumb:hover {
background: #555;
}
.pulse-animation {
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);
}
}
</style>
</head>
<body>
<!-- Hero Section -->
<div class="relative min-h-screen flex items-center justify-center text-white">
<div class="absolute inset-0 bg-black opacity-50"></div>
<div class="relative z-10 text-center px-4 max-w-6xl mx-auto">
<h1 class="text-5xl md:text-7xl font-bold mb-6" data-aos="fade-up">
TPU芯片
</h1>
<p class="text-xl md:text-3xl mb-8" data-aos="fade-up" data-aos-delay="200">
AI算力的架构革命
</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12" data-aos="fade-up" data-aos-delay="400">
<div class="glass-effect p-6 rounded-2xl">
<div class="text-4xl font-bold mb-2">4614</div>
<div class="text-lg">TFLOPS算力</div>
<div class="text-sm mt-2 opacity-80">TPU v7 (Ironwood)</div>
</div>
<div class="glass-effect p-6 rounded-2xl">
<div class="text-4xl font-bold mb-2">980万亿</div>
<div class="text-lg">Tokens调用量</div>
<div class="text-sm mt-2 opacity-80">2025年7月预期</div>
</div>
<div class="glass-effect p-6 rounded-2xl">
<div class="text-4xl font-bold mb-2">3-5倍</div>
<div class="text-lg">性价比优势</div>
<div class="text-sm mt-2 opacity-80">对比GPU</div>
</div>
</div>
</div>
<div class="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</div>
</div>
<!-- Timeline Section -->
<div class="py-20 bg-white">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-16 gradient-text">发展时间轴</h2>
<div class="relative">
<div class="timeline-line hidden md:block"></div>
<div class="space-y-12">
<div class="flex flex-col md:flex-row items-center" data-aos="fade-right">
<div class="md:w-1/2 md:pr-8 text-right">
<div class="glass-effect p-6 rounded-xl inline-block">
<h3 class="text-2xl font-bold mb-2">2023年12月</h3>
<p class="text-gray-600">发布TPU v5p性能较v4提升2.8倍</p>
</div>
</div>
<div class="timeline-dot w-4 h-4 bg-purple-600 rounded-full mx-4 my-4"></div>
<div class="md:w-1/2 md:pl-8"></div>
</div>
<div class="flex flex-col md:flex-row items-center" data-aos="fade-left">
<div class="md:w-1/2 md:pr-8"></div>
<div class="timeline-dot w-4 h-4 bg-purple-600 rounded-full mx-4 my-4"></div>
<div class="md:w-1/2 md:pl-8">
<div class="glass-effect p-6 rounded-xl inline-block">
<h3 class="text-2xl font-bold mb-2">2024年8月</h3>
<p class="text-gray-600">苹果使用8192颗TPU v4训练AI模型</p>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row items-center" data-aos="fade-right">
<div class="md:w-1/2 md:pr-8 text-right">
<div class="glass-effect p-6 rounded-xl inline-block pulse-animation">
<h3 class="text-2xl font-bold mb-2">2025年4月9日</h3>
<p class="text-gray-600">TPU v7 (Ironwood)正式发布</p>
<p class="text-sm text-purple-600 mt-2">4614 TFLOPS算力 · 192GB HBM3e</p>
</div>
</div>
<div class="timeline-dot w-4 h-4 bg-purple-600 rounded-full mx-4 my-4"></div>
<div class="md:w-1/2 md:pl-8"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Core Logic Section -->
<div class="py-20 bg-gray-50">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-16 gradient-text">核心驱动力</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="stat-card bg-white p-8 rounded-2xl shadow-xl" data-aos="zoom-in" data-aos-delay="100">
<div class="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path>
</svg>
</div>
<h3 class="text-xl font-bold mb-4">硬件架构颠覆</h3>
<p class="text-gray-600 mb-4">脉动阵列 + 3D Torus网络拓扑</p>
<div class="text-sm text-purple-600 font-semibold">
利用率: 50%+ (GPU仅20-40%)
</div>
</div>
<div class="stat-card bg-white p-8 rounded-2xl shadow-xl" data-aos="zoom-in" data-aos-delay="200">
<div class="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<h3 class="text-xl font-bold mb-4">TCO碾压优势</h3>
<p class="text-gray-600 mb-4">租赁成本仅为H100的1/4</p>
<div class="text-sm text-purple-600 font-semibold">
H100: 7万美元/月 → TPU: 3万美元/月
</div>
</div>
<div class="stat-card bg-white p-8 rounded-2xl shadow-xl" data-aos="zoom-in" data-aos-delay="300">
<div class="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<h3 class="text-xl font-bold mb-4">生态开放拐点</h3>
<p class="text-gray-600 mb-4">TPU+XLA对标GPU+CUDA</p>
<div class="text-sm text-purple-600 font-semibold">
Meta、OpenAI等外部客户接入
</div>
</div>
</div>
</div>
</div>
<!-- Industry Chain Section -->
<div class="py-20 bg-white">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-16 gradient-text">产业链价值分布</h2>
<div class="mb-12">
<canvas id="valueChart" width="400" height="200"></canvas>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
<div class="p-4 bg-purple-50 rounded-lg">
<div class="text-2xl font-bold text-purple-600">30-35%</div>
<div class="text-gray-600">PCB</div>
</div>
<div class="p-4 bg-blue-50 rounded-lg">
<div class="text-2xl font-bold text-blue-600">20-25%</div>
<div class="text-gray-600">HBM</div>
</div>
<div class="p-4 bg-green-50 rounded-lg">
<div class="text-2xl font-bold text-green-600">15-20%</div>
<div class="text-gray-600">电源模块</div>
</div>
<div class="p-4 bg-yellow-50 rounded-lg">
<div class="text-2xl font-bold text-yellow-600">10-15%</div>
<div class="text-gray-600">OCS光交换</div>
</div>
</div>
</div>
</div>
<!-- Stocks Table Section -->
<div class="py-20 bg-gray-50">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-16 gradient-text">相关标的</h2>
<div class="table-responsive bg-white rounded-2xl shadow-xl overflow-hidden">
<table class="w-full">
<thead class="bg-gradient-to-r from-purple-600 to-purple-800 text-white">
<tr>
<th class="px-6 py-4 text-left">股票名称</th>
<th class="px-6 py-4 text-left">分类</th>
<th class="px-6 py-4 text-left">相关性</th>
<th class="px-6 py-4 text-left">信源</th>
<th class="px-6 py-4 text-center">优先级</th>
</tr>
</thead>
<tbody>
<tr class="border-b hover:bg-purple-50 transition-colors">
<td class="px-6 py-4 font-semibold">光库科技</td>
<td class="px-6 py-4"><span class="badge badge-primary">OCS光交换</span></td>
<td class="px-6 py-4">谷歌OCS独家代工厂单台3万美元</td>
<td class="px-6 py-4">网传纪要</td>
<td class="px-6 py-4 text-center"><span class="text-2xl">⭐⭐⭐⭐⭐</span></td>
</tr>
<tr class="border-b hover:bg-purple-50 transition-colors">
<td class="px-6 py-4 font-semibold">新雷能</td>
<td class="px-6 py-4"><span class="badge badge-secondary">电源</span></td>
<td class="px-6 py-4">意向订单超5亿美元国产替代</td>
<td class="px-6 py-4">网传纪要</td>
<td class="px-6 py-4 text-center"><span class="text-2xl">⭐⭐⭐⭐⭐</span></td>
</tr>
<tr class="border-b hover:bg-purple-50 transition-colors">
<td class="px-6 py-4 font-semibold">胜宏科技</td>
<td class="px-6 py-4"><span class="badge badge-accent">PCB</span></td>
<td class="px-6 py-4">V7大份额一供价值量翻倍</td>
<td class="px-6 py-4">网传纪要</td>
<td class="px-6 py-4 text-center"><span class="text-2xl">⭐⭐⭐⭐</span></td>
</tr>
<tr class="border-b hover:bg-purple-50 transition-colors">
<td class="px-6 py-4 font-semibold">沪电股份</td>
<td class="px-6 py-4"><span class="badge badge-accent">PCB</span></td>
<td class="px-6 py-4">供应份额30-40%主导30-40层板</td>
<td class="px-6 py-4">网传纪要</td>
<td class="px-6 py-4 text-center"><span class="text-2xl">⭐⭐⭐⭐</span></td>
</tr>
<tr class="border-b hover:bg-purple-50 transition-colors">
<td class="px-6 py-4 font-semibold">中际旭创</td>
<td class="px-6 py-4"><span class="badge badge-info">光模块</span></td>
<td class="px-6 py-4">谷歌份额60%+,确定性最高</td>
<td class="px-6 py-4">网传纪要</td>
<td class="px-6 py-4 text-center"><span class="text-2xl">⭐⭐⭐⭐</span></td>
</tr>
<tr class="border-b hover:bg-purple-50 transition-colors">
<td class="px-6 py-4 font-semibold">东材科技</td>
<td class="px-6 py-4"><span class="badge badge-warning">M9材料</span></td>
<td class="px-6 py-4">台光核心高速树脂主力供应商</td>
<td class="px-6 py-4">网传纪要</td>
<td class="px-6 py-4 text-center"><span class="text-2xl">⭐⭐⭐</span></td>
</tr>
<tr class="border-b hover:bg-purple-50 transition-colors">
<td class="px-6 py-4 font-semibold">天普股份</td>
<td class="px-6 py-4"><span class="badge badge-error">国产TPU</span></td>
<td class="px-6 py-4">中昊芯英拟要约收购</td>
<td class="px-6 py-4">公告</td>
<td class="px-6 py-4 text-center"><span class="text-2xl">⭐⭐</span></td>
</tr>
<tr class="hover:bg-purple-50 transition-colors">
<td class="px-6 py-4 font-semibold">深南电路</td>
<td class="px-6 py-4"><span class="badge badge-accent">PCB</span></td>
<td class="px-6 py-4">供应V7 44层板份额10-15%</td>
<td class="px-6 py-4">网传纪要</td>
<td class="px-6 py-4 text-center"><span class="text-2xl">⭐⭐⭐</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Risks Section -->
<div class="py-20 bg-white">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-16 gradient-text">潜在风险</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="alert alert-warning shadow-lg" data-aos="fade-up">
<svg class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<div>
<h3 class="font-bold">软件生态成熟度</h3>
<div class="text-xs">TPU+XLA生态仍落后CUDA 5年以上</div>
</div>
</div>
<div class="alert alert-error shadow-lg" data-aos="fade-up" data-aos-delay="100">
<svg class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">HBM供应瓶颈</h3>
<div class="text-xs">2025年HBM产能被英伟达抢占</div>
</div>
</div>
<div class="alert alert-info shadow-lg" data-aos="fade-up" data-aos-delay="200">
<svg class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">客户集中度风险</h3>
<div class="text-xs">85%需求来自谷歌内部</div>
</div>
</div>
<div class="alert alert-warning shadow-lg" data-aos="fade-up" data-aos-delay="300">
<svg class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<div>
<h3 class="font-bold">架构专利壁垒</h3>
<div class="text-xs">国产TPU面临侵权风险</div>
</div>
</div>
<div class="alert alert-error shadow-lg" data-aos="fade-up" data-aos-delay="400">
<svg class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">同业追赶</h3>
<div class="text-xs">Meta、AWS ASIC 2026年量产</div>
</div>
</div>
<div class="alert alert-info shadow-lg" data-aos="fade-up" data-aos-delay="500">
<svg class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">应用场景局限</h3>
<div class="text-xs">新兴架构(Mamba)适配性差</div>
</div>
</div>
</div>
</div>
</div>
<!-- Catalysts Section -->
<div class="py-20 bg-gray-50">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-16 gradient-text">关键催化剂</h2>
<div class="timeline">
<div class="timeline-item" data-aos="fade-up">
<div class="timeline-marker bg-purple-600"></div>
<div class="timeline-content">
<h3 class="text-xl font-bold mb-2">近期 (2025Q2-Q4)</h3>
<ul class="list-disc list-inside text-gray-600 space-y-1">
<li>Ironwood量产验证与正式上架</li>
<li>Anthropic 100万颗TPU订单交付</li>
<li>供应链订单落地胜宏、光库Q2财报</li>
<li>国产TPU产业化突破</li>
</ul>
</div>
</div>
<div class="timeline-item" data-aos="fade-up" data-aos-delay="200">
<div class="timeline-marker bg-blue-600"></div>
<div class="timeline-content">
<h3 class="text-xl font-bold mb-2">中期 (2025-2026)</h3>
<ul class="list-disc list-inside text-gray-600 space-y-1">
<li>JAX XLA生态开放给第三方开发者</li>
<li>产业链进入量价齐升阶段</li>
<li>国产TPU在特定领域落地</li>
</ul>
</div>
</div>
<div class="timeline-item" data-aos="fade-up" data-aos-delay="400">
<div class="timeline-marker bg-green-600"></div>
<div class="timeline-content">
<h3 class="text-xl font-bold mb-2">长期 (2025-2027)</h3>
<ul class="list-disc list-inside text-gray-600 space-y-1">
<li>AI ASIC市场750亿美元三分天下</li>
<li>TPU推理市场份额超40%</li>
<li>HDI技术替代高多层PCB</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Conclusion Section -->
<div class="py-20 bg-gradient-to-r from-purple-600 to-purple-800 text-white">
<div class="max-w-4xl mx-auto px-4 text-center">
<h2 class="text-4xl font-bold mb-8" data-aos="fade-up">投资启示</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
<div class="glass-effect p-6 rounded-2xl" data-aos="fade-up" data-aos-delay="100">
<h3 class="text-2xl font-bold mb-4">💡 核心策略</h3>
<p class="text-lg">"抓两头,放中间"</p>
<ul class="text-left mt-4 space-y-2">
<li>• 抓"增量"OCS、电源0到1机会</li>
<li>• 抓"龙头"PCB量价齐升</li>
<li>• 避开"伪主题"国产TPU专利风险</li>
</ul>
</div>
<div class="glass-effect p-6 rounded-2xl" data-aos="fade-up" data-aos-delay="200">
<h3 class="text-2xl font-bold mb-4">🎯 最具价值环节</h3>
<div class="space-y-3 text-left">
<div class="flex justify-between items-center">
<span>光库科技(OCS)</span>
<span class="text-yellow-300">★★★★★</span>
</div>
<div class="flex justify-between items-center">
<span>新雷能(电源)</span>
<span class="text-yellow-300">★★★★★</span>
</div>
<div class="flex justify-between items-center">
<span>胜宏/沪电(PCB)</span>
<span class="text-yellow-300">★★★★</span>
</div>
</div>
</div>
</div>
<div class="text-xl italic" data-aos="fade-up" data-aos-delay="300">
"TPU不是GPU的简单替代而是AI算力架构的重新定义"
</div>
</div>
</div>
<!-- Footer -->
<footer class="bg-gray-900 text-white py-8">
<div class="max-w-7xl mx-auto px-4 text-center">
<p class="text-sm opacity-75">数据来源新闻、路演、Insight分析 | 更新时间2025年</p>
<p class="text-xs mt-2 opacity-50">注:投资有风险,本文仅供参考</p>
</div>
</footer>
<script>
// Initialize AOS
AOS.init({
duration: 1000,
once: true
});
// Chart.js for value distribution
const ctx = document.getElementById('valueChart').getContext('2d');
const valueChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['PCB', 'HBM', '电源模块', 'OCS光交换', '光模块', '其他'],
datasets: [{
data: [32.5, 22.5, 17.5, 12.5, 7.5, 7.5],
backgroundColor: [
'rgba(147, 51, 234, 0.8)',
'rgba(59, 130, 246, 0.8)',
'rgba(34, 197, 94, 0.8)',
'rgba(250, 204, 21, 0.8)',
'rgba(239, 68, 68, 0.8)',
'rgba(107, 114, 128, 0.8)'
],
borderColor: [
'rgba(147, 51, 234, 1)',
'rgba(59, 130, 246, 1)',
'rgba(34, 197, 94, 1)',
'rgba(250, 204, 21, 1)',
'rgba(239, 68, 68, 1)',
'rgba(107, 114, 128, 1)'
],
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
padding: 20,
font: {
size: 14
}
}
},
tooltip: {
callbacks: {
label: function(context) {
return context.label + ': ' + context.parsed + '%';
}
}
}
}
}
});
// Smooth scroll
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
</script>
</body>
</html>

876
public/htmls/海军.html Normal file
View File

@@ -0,0 +1,876 @@
<!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@3.4.1/dist/tailwind.min.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
* {
font-family: 'Inter', sans-serif;
}
.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.3);
}
.timeline-line {
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
width: 3px;
position: absolute;
left: 20px;
top: 0;
bottom: 0;
}
.timeline-dot {
width: 16px;
height: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 3px solid #1e293b;
border-radius: 50%;
position: absolute;
left: 14px;
}
.progress-ring {
transform: rotate(-90deg);
}
.navy-blue {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
}
.glass-effect {
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-scroll::-webkit-scrollbar {
height: 8px;
}
.table-scroll::-webkit-scrollbar-track {
background: #1e293b;
}
.table-scroll::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
.badge-glow {
animation: glow 2s ease-in-out infinite;
}
@keyframes glow {
0%, 100% { box-shadow: 0 0 5px rgba(102, 126, 234, 0.5); }
50% { box-shadow: 0 0 20px rgba(102, 126, 234, 0.8); }
}
.metric-card {
background: linear-gradient(135deg, rgba(30, 58, 138, 0.5) 0%, rgba(30, 64, 175, 0.5) 100%);
border: 1px solid rgba(147, 197, 253, 0.3);
}
.stock-table {
font-size: 0.875rem;
}
.stock-table th {
background: rgba(30, 58, 138, 0.9);
position: sticky;
top: 0;
z-index: 10;
}
.stock-table td {
border-bottom: 1px solid rgba(71, 85, 105, 0.5);
}
.stock-table tr:hover {
background: rgba(51, 65, 85, 0.3);
}
.section-divider {
height: 2px;
background: linear-gradient(90deg, transparent, #667eea, transparent);
margin: 3rem 0;
}
</style>
</head>
<body class="bg-slate-950 text-slate-100">
<!-- Navigation -->
<div class="navbar glass-effect fixed top-0 z-50 px-4">
<div class="navbar-start">
<a href="#" class="btn btn-ghost text-xl font-bold">
<i class="fas fa-ship mr-2"></i>海军装备分析
</a>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li><a href="#overview">核心观点</a></li>
<li><a href="#timeline">事件时间线</a></li>
<li><a href="#logic">投资逻辑</a></li>
<li><a href="#industry">产业链</a></li>
<li><a href="#stocks">标的池</a></li>
</ul>
</div>
<div class="navbar-end">
<div class="badge badge-accent badge-glow">深度研究</div>
</div>
</div>
<!-- Hero Section -->
<section class="hero min-h-screen hero-gradient flex items-center">
<div class="hero-content text-center">
<div class="max-w-5xl">
<h1 class="text-5xl font-bold mb-6 text-white">
<i class="fas fa-anchor mr-3"></i>海军装备产业链
</h1>
<p class="text-2xl mb-8 text-slate-200">战略需求刚性化 · 装备结构拐点化 · 业绩兑现初期化</p>
<div class="stats shadow-2xl bg-slate-900/80 backdrop-blur rounded-box w-full">
<div class="stat">
<div class="stat-figure text-primary">
<svg width="48" height="48" fill="currentColor"><path d="M24 2l5.453 11.146 12.298 1.787-8.876 8.654 2.095 12.224L24 30.077 13.13 35.811l2.095-12.224-8.876-8.654 12.298-1.787L24 2z"/></svg>
</div>
<div class="stat-title">优先级</div>
<div class="stat-value text-primary">极高</div>
<div class="stat-desc">十四五重点建设方向</div>
</div>
<div class="stat">
<div class="stat-figure text-secondary">
<svg class="progress-ring" width="48" height="48" viewBox="0 0 36 36">
<path class="text-slate-700" stroke="currentColor" stroke-width="3" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
<path class="text-slate-100" stroke="currentColor" stroke-width="3" stroke-dasharray="75, 100" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
</svg>
</div>
<div class="stat-title">发展阶段</div>
<div class="stat-value text-secondary">初期</div>
<div class="stat-desc">0-1的蓝海市场</div>
</div>
<div class="stat">
<div class="stat-figure text-accent">
<i class="fas fa-chart-line text-4xl"></i>
</div>
<div class="stat-title">业绩弹性</div>
<div class="stat-value text-accent">25%+</div>
<div class="stat-desc">2025年预期增速</div>
</div>
</div>
<div class="mt-8 flex justify-center gap-4">
<button class="btn btn-primary btn-lg">
<i class="fas fa-download mr-2"></i>下载研报
</button>
<button class="btn btn-secondary btn-lg">
<i class="fas fa-play-circle mr-2"></i>观看路演
</button>
</div>
</div>
</div>
</section>
<!-- Key Metrics -->
<section class="py-16 px-4">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold text-center mb-12" id="overview">核心数据洞察</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="metric-card rounded-xl p-6 card-hover">
<div class="flex items-center justify-between">
<div>
<p class="text-slate-400">水下装备市场空间</p>
<p class="text-3xl font-bold text-blue-400">近千亿</p>
</div>
<i class="fas fa-water text-4xl text-blue-400/30"></i>
</div>
<div class="mt-4">
<div class="text-sm text-slate-500">未来10年需求</div>
</div>
</div>
<div class="metric-card rounded-xl p-6 card-hover">
<div class="flex items-center justify-between">
<div>
<p class="text-slate-400">电磁弹射弹性</p>
<p class="text-3xl font-bold text-purple-400">5-8亿</p>
</div>
<i class="fas fa-bolt text-4xl text-purple-400/30"></i>
</div>
<div class="mt-4">
<div class="text-sm text-slate-500">2025年增量收入</div>
</div>
</div>
<div class="metric-card rounded-xl p-6 card-hover">
<div class="flex items-center justify-between">
<div>
<p class="text-slate-400">市占率龙头</p>
<p class="text-3xl font-bold text-green-400">70-80%</p>
</div>
<i class="fas fa-crown text-4xl text-green-400/30"></i>
</div>
<div class="mt-4">
<div class="text-sm text-slate-500">中国海防水声业务</div>
</div>
</div>
<div class="metric-card rounded-xl p-6 card-hover">
<div class="flex items-center justify-between">
<div>
<p class="text-slate-400">装备增速</p>
<p class="text-3xl font-bold text-orange-400">20-30%</p>
</div>
<i class="fas fa-rocket text-4xl text-orange-400/30"></i>
</div>
<div class="mt-4">
<div class="text-sm text-slate-500">十四五后期预期</div>
</div>
</div>
</div>
</div>
</section>
<!-- Timeline -->
<section class="py-16 px-4" id="timeline">
<div class="max-w-4xl mx-auto">
<h2 class="text-4xl font-bold text-center mb-12">重要事件时间线</h2>
<div class="relative">
<div class="timeline-line"></div>
<div class="relative mb-8 ml-12">
<div class="timeline-dot" style="top: 8px;"></div>
<div class="card glass-effect rounded-lg p-4 card-hover">
<div class="flex justify-between items-start">
<div>
<span class="badge badge-primary">2024年8月</span>
<h3 class="text-xl font-semibold mt-2">伊朗海军演习</h3>
<p class="text-slate-400 mt-1">在阿曼海举行"力量1404"导弹演习,展示反舰能力</p>
</div>
</div>
</div>
</div>
<div class="relative mb-8 ml-12">
<div class="timeline-dot" style="top: 8px;"></div>
<div class="card glass-effect rounded-lg p-4 card-hover">
<div class="flex justify-between items-start">
<div>
<span class="badge badge-primary">2024年9月</span>
<h3 class="text-xl font-semibold mt-2">以色列军事行动</h3>
<p class="text-slate-400 mt-1">摧毁叙利亚舰队,地区紧张局势升级</p>
</div>
</div>
</div>
</div>
<div class="relative mb-8 ml-12">
<div class="timeline-dot" style="top: 8px;"></div>
<div class="card glass-effect rounded-lg p-4 card-hover">
<div class="flex justify-between items-start">
<div>
<span class="badge badge-secondary">2024年12月</span>
<h3 class="text-xl font-semibold mt-2">076型四川舰出坞</h3>
<p class="text-slate-400 mt-1">首创双舰岛+电磁弹射技术排水量4万余吨</p>
</div>
</div>
</div>
</div>
<div class="relative mb-8 ml-12">
<div class="timeline-dot" style="top: 8px;"></div>
<div class="card glass-effect rounded-lg p-4 card-hover">
<div class="flex justify-between items-start">
<div>
<span class="badge badge-accent">2024年12月</span>
<h3 class="text-xl font-semibold mt-2">国防部长任命</h3>
<p class="text-slate-400 mt-1">董军(原海军首长)任国防部长,海军重点建设预期强化</p>
</div>
</div>
</div>
</div>
<div class="relative mb-8 ml-12">
<div class="timeline-dot" style="top: 8px;"></div>
<div class="card glass-effect rounded-lg p-4 card-hover">
<div class="flex justify-between items-start">
<div>
<span class="badge badge-info">2025年3月</span>
<h3 class="text-xl font-semibold mt-2">俄罗斯海军战略</h3>
<p class="text-slate-400 mt-1">普京批准《2050年前俄罗斯海军发展战略》</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Investment Logic -->
<section class="py-16 px-4" id="logic">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold text-center mb-12">投资逻辑分析</h2>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="card navy-blue rounded-xl p-6">
<div class="card-title">
<i class="fas fa-globe-americas text-3xl mr-3"></i>
<span>地缘政治倒逼</span>
</div>
<div class="card-body">
<p>美军"印太战略"持续施压弗吉尼亚级潜艇年产目标从1.3艘提升至2.5艘,直接刺激中国水下攻防体系建设</p>
<div class="mt-4">
<span class="badge badge-error">高优先级</span>
<span class="badge badge-warning">紧迫性强</span>
</div>
</div>
</div>
<div class="card navy-blue rounded-xl p-6">
<div class="card-title">
<i class="fas fa-exchange-alt text-3xl mr-3"></i>
<span>战略范式转型</span>
</div>
<div class="card-body">
<p>从"黄水海军"到"蓝水海军"质变水面防御向水下进攻转变10年潜艇改造市场近千亿</p>
<div class="mt-4">
<span class="badge badge-success">结构性机会</span>
<span class="badge badge-info">长期趋势</span>
</div>
</div>
</div>
<div class="card navy-blue rounded-xl p-6">
<div class="card-title">
<i class="fas fa-microchip text-3xl mr-3"></i>
<span>技术代际突破</span>
</div>
<div class="card-body">
<p>076型电磁弹射平台型创新光纤水听器替代压电水听器技术壁垒高市场空间巨大</p>
<div class="mt-4">
<span class="badge badge-primary">核心技术</span>
<span class="badge badge-accent">高壁垒</span>
</div>
</div>
</div>
</div>
<div class="section-divider"></div>
<h3 class="text-2xl font-bold mb-6">预期差分析</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<div>
<h4 class="font-bold">业绩兑现节奏</h4>
<p>潜艇改造周期18-24个月远快于新造5-7年</p>
</div>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<div>
<h4 class="font-bold">民用市场空间</h4>
<p>电磁弹射技术可延伸至海上风电等民用领域</p>
</div>
</div>
<div class="alert alert-success">
<i class="fas fa-shield-alt"></i>
<div>
<h4 class="font-bold">竞争壁垒</h4>
<p>设计锁定+耗材更换的双重护城河</p>
</div>
</div>
</div>
</div>
</section>
<!-- Industry Chain -->
<section class="py-16 px-4" id="industry">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold text-center mb-12">产业链图谱</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="card glass-effect rounded-xl p-6">
<h3 class="text-2xl font-bold mb-4 text-blue-400">
<i class="fas fa-anchor mr-2"></i>水面舰艇产业链
</h3>
<div class="space-y-3">
<div class="flex justify-between items-center p-3 bg-slate-800/50 rounded-lg">
<span class="font-medium">上游材料</span>
<div class="flex gap-2">
<span class="badge badge-outline">西部材料</span>
<span class="badge badge-outline">亚星锚链</span>
</div>
</div>
<div class="flex justify-between items-center p-3 bg-slate-800/50 rounded-lg">
<span class="font-medium">中游设备</span>
<div class="flex gap-2">
<span class="badge badge-primary">湘电股份</span>
<span class="badge badge-primary">王子新材</span>
</div>
</div>
<div class="flex justify-between items-center p-3 bg-slate-800/50 rounded-lg">
<span class="font-medium">下游总装</span>
<div class="flex gap-2">
<span class="badge badge-secondary">中国船舶</span>
<span class="badge badge-secondary">中船防务</span>
</div>
</div>
</div>
</div>
<div class="card glass-effect rounded-xl p-6">
<h3 class="text-2xl font-bold mb-4 text-purple-400">
<i class="fas fa-water mr-2"></i>水下攻防产业链
</h3>
<div class="space-y-3">
<div class="flex justify-between items-center p-3 bg-slate-800/50 rounded-lg">
<span class="font-medium">上游材料</span>
<div class="flex gap-2">
<span class="badge badge-outline">西部材料</span>
<span class="badge badge-outline">宝钛股份</span>
</div>
</div>
<div class="flex justify-between items-center p-3 bg-slate-800/50 rounded-lg">
<span class="font-medium">中游核心系统</span>
<div class="flex gap-2">
<span class="badge badge-accent">中国海防</span>
<span class="badge badge-accent">中科海讯</span>
</div>
</div>
<div class="flex justify-between items-center p-3 bg-slate-800/50 rounded-lg">
<span class="font-medium">下游平台</span>
<div class="flex gap-2">
<span class="badge badge-info">中国重工</span>
<span class="badge badge-info">天海防务</span>
</div>
</div>
</div>
</div>
</div>
<div class="mt-8">
<h3 class="text-2xl font-bold mb-6">核心玩家对比</h3>
<div class="table-scroll">
<table class="table w-full stock-table">
<thead>
<tr>
<th>公司</th>
<th>核心逻辑</th>
<th>竞争优势</th>
<th>业绩弹性</th>
<th>关键风险</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-bold text-primary">中国海防</td>
<td>水下攻防装备龙头</td>
<td>70-80%市占率</td>
<td>⭐⭐⭐⭐⭐</td>
<td>订单验证周期长</td>
</tr>
<tr>
<td class="font-bold text-primary">湘电股份</td>
<td>电磁弹射平台技术</td>
<td>综合电力系统垄断</td>
<td>⭐⭐⭐⭐</td>
<td>技术可靠性待检验</td>
</tr>
<tr>
<td class="font-bold text-primary">西部材料</td>
<td>水下装备材料</td>
<td>钛合金核心供应商</td>
<td>⭐⭐⭐</td>
<td>军品占比低</td>
</tr>
<tr>
<td class="font-bold text-primary">中国船舶</td>
<td>海军装备总装</td>
<td>航母唯一建造商</td>
<td>⭐⭐</td>
<td>弹性相对较小</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Stock Pool -->
<section class="py-16 px-4" id="stocks">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold text-center mb-12">海军概念股票池</h2>
<div class="table-scroll">
<table class="table w-full stock-table">
<thead>
<tr>
<th class="sticky left-0 bg-slate-900">股票名称</th>
<th>所属行业</th>
<th>核心项目</th>
<th>产业链环节</th>
<th>投资逻辑</th>
<th>数据来源</th>
</tr>
</thead>
<tbody>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">中国船舶</td>
<td>船舶制造</td>
<td>航母、核潜艇、大型驱逐舰</td>
<td>海军装备研发生产</td>
<td>民用商用船舶制造营收第一,承担海军主战装备科研生产任务</td>
<td><span class="badge badge-outline">公开资料</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">中船防务</td>
<td>船舶制造</td>
<td>055型驱逐舰</td>
<td>护卫舰市场</td>
<td>在护卫舰市场占据领先地位,参与多个重点型号建造</td>
<td><span class="badge badge-outline">公开资料</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">中国海防</td>
<td>海洋防务</td>
<td>声纳装备</td>
<td>水声电子系统</td>
<td>国内海洋防务声纳装备核心供应商市占率70-80%</td>
<td><span class="badge badge-outline">互动</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">湘电股份</td>
<td>船舶动力</td>
<td>电磁弹射系统</td>
<td>舰船电力推进</td>
<td>国内唯一舰船综合电力系统供应商,电磁弹射核心分包商</td>
<td><span class="badge badge-outline">路演数据</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">西部材料</td>
<td>金属材料</td>
<td>钛合金结构件</td>
<td>特种材料</td>
<td>钛合金用于核潜艇壳体、声纳结构件单艘价值量5-6亿</td>
<td><span class="badge badge-outline">路演数据</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">中国动力</td>
<td>船舶动力</td>
<td>海军舰船动力</td>
<td>动力系统</td>
<td>海军作战舰艇动力系统排名第一,主要供应商</td>
<td><span class="badge badge-outline">互动</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">中国重工</td>
<td>船舶制造</td>
<td>核潜艇</td>
<td>水下装备总装</td>
<td>承担核潜艇建造任务,水下装备核心总装单位</td>
<td><span class="badge badge-outline">公开资料</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">中科海讯</td>
<td>声纳装备</td>
<td>水声目标探测</td>
<td>水声电子</td>
<td>产品应用于声纳装备领域,最终用户为国家特种部门</td>
<td><span class="badge badge-outline">互动</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">天海防务</td>
<td>特种无人船</td>
<td>无人潜航器</td>
<td>无人装备平台</td>
<td>子公司金海运研发多类型特种无人船,无人装备标的</td>
<td><span class="badge badge-outline">互动</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">久之洋</td>
<td>光电设备</td>
<td>红外、激光、光学产品</td>
<td>舰载光电系统</td>
<td>中船集团旗下光电上市公司,产品涵盖四大类光电设备</td>
<td><span class="badge badge-outline">互动</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">亚光科技</td>
<td>军用小艇</td>
<td>冲锋舟、指挥艇</td>
<td>特种舰艇</td>
<td>军用冲锋舟、指挥艇等特种小艇市占率领先</td>
<td><span class="badge badge-outline">公开资料</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">亚星锚链</td>
<td>链条生产</td>
<td>船用锚链</td>
<td>船舶配套</td>
<td>全球最大链条生产企业船用锚链市占率超70%</td>
<td><span class="badge badge-outline">公开资料</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">中船科技</td>
<td>船舶设计</td>
<td>船舶工业规划设计</td>
<td>设计研发</td>
<td>全资子公司中船九院为中国船舶工业规划设计国家队</td>
<td><span class="badge badge-outline">公开资料</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">海兰信</td>
<td>船舶电子</td>
<td>首艘航母辽宁舰</td>
<td>舰船电子系统</td>
<td>产品已应用于包括辽宁舰在内的各类舰船</td>
<td><span class="badge badge-outline">公告</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">国瑞科技</td>
<td>船舶电子</td>
<td>雷达、电子战系统</td>
<td>军工电子信息系统</td>
<td>参股公司中电华瑞产品涵盖雷达、电子战系统</td>
<td><span class="badge badge-outline">网传纪要</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">泰豪科技</td>
<td>舰载作战辅助系统</td>
<td>航母及海军舰艇</td>
<td>作战支持系统</td>
<td>相关产品已应用于航母及海军舰艇</td>
<td><span class="badge badge-outline">互动</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">潍柴重机</td>
<td>船舶动力</td>
<td>船舶动力系统</td>
<td>动力设备</td>
<td>开发销售船舶动力和发电设备,市场覆盖广泛</td>
<td><span class="badge badge-outline">互动</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">江龙船艇</td>
<td>公务执法船艇</td>
<td>海事海警装备</td>
<td>特种船艇</td>
<td>产品广泛应用于海事、海关、海警等维护海洋主权领域</td>
<td><span class="badge badge-outline">互动</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">王子新材</td>
<td>船舶电子</td>
<td>舰船电子信息系统</td>
<td>电子配套</td>
<td>全资子公司中电华瑞从事舰船电子信息系统领域</td>
<td><span class="badge badge-outline">互动</span></td>
</tr>
<tr>
<td class="sticky left-0 font-bold bg-slate-800">*ST宝实</td>
<td>船舶电器</td>
<td>船舶消磁器</td>
<td>船舶配套</td>
<td>船舶消磁器业务市场占有率居首位</td>
<td><span class="badge badge-outline">年报</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Investment Strategy -->
<section class="py-16 px-4">
<div class="max-w-7xl mx-auto">
<h2 class="text-4xl font-bold text-center mb-12">投资策略建议</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="card bg-gradient-to-r from-green-900/50 to-green-800/30 rounded-xl p-6">
<h3 class="text-2xl font-bold mb-4 text-green-400">
<i class="fas fa-thumbs-up mr-2"></i>重点配置
</h3>
<div class="space-y-3">
<div class="p-3 bg-slate-800/50 rounded-lg">
<div class="flex items-center justify-between">
<span class="font-bold">中国海防</span>
<span class="badge badge-success">首选标的</span>
</div>
<p class="text-sm text-slate-400 mt-1">70%市占率+资产注入预期+业绩拐点三重逻辑</p>
</div>
<div class="p-3 bg-slate-800/50 rounded-lg">
<div class="flex items-center justify-between">
<span class="font-bold">湘电股份</span>
<span class="badge badge-success">弹性标的</span>
</div>
<p class="text-sm text-slate-400 mt-1">电磁弹射0到1突破2025年收入确认高峰</p>
</div>
<div class="p-3 bg-slate-800/50 rounded-lg">
<div class="flex items-center justify-between">
<span class="font-bold">西部材料</span>
<span class="badge badge-success">稳健标的</span>
</div>
<p class="text-sm text-slate-400 mt-1">钛合金刚需,不受军品定价机制约束</p>
</div>
</div>
</div>
<div class="card bg-gradient-to-r from-red-900/50 to-red-800/30 rounded-xl p-6">
<h3 class="text-2xl font-bold mb-4 text-red-400">
<i class="fas fa-exclamation-triangle mr-2"></i>规避标的
</h3>
<div class="space-y-3">
<div class="p-3 bg-slate-800/50 rounded-lg">
<div class="flex items-center justify-between">
<span class="font-bold">王子新材</span>
<span class="badge badge-error">伪概念</span>
</div>
<p class="text-sm text-slate-400 mt-1">军品占比不足5%,与电磁弹射无技术关联</p>
</div>
<div class="p-3 bg-slate-800/50 rounded-lg">
<div class="flex items-center justify-between">
<span class="font-bold">久之洋</span>
<span class="badge badge-error">业绩下滑</span>
</div>
<p class="text-sm text-slate-400 mt-1">民用海监设备为主2024H1收入同比-22%</p>
</div>
<div class="p-3 bg-slate-800/50 rounded-lg">
<div class="flex items-center justify-between">
<span class="font-bold">海兰信</span>
<span class="badge badge-error">ST风险</span>
</div>
<p class="text-sm text-slate-400 mt-1">军品资质不全2024年亏损1.2亿</p>
</div>
</div>
</div>
</div>
<div class="section-divider"></div>
<div class="alert alert-info">
<i class="fas fa-info-circle text-2xl"></i>
<div>
<h4 class="font-bold text-lg">关键验证指标</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<span class="font-semibold">订单指标:</span>
<p>全军装备采购信息网海军装备招标金额</p>
</div>
<div>
<span class="font-semibold">财务指标:</span>
<p>中国海防合同负债环比增速需持续增长30%+</p>
</div>
<div>
<span class="font-semibold">技术节点:</span>
<p>076型舰海试进度与电磁弹射测试结果</p>
</div>
<div>
<span class="font-semibold">政策信号:</span>
<p>2025年国防预算海军科目增速需超20%</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer footer-center p-10 bg-slate-900 text-base-content">
<div>
<p class="font-bold text-2xl">
<i class="fas fa-ship mr-2"></i>海军装备产业链深度分析
</p>
<p class="text-slate-400">专业投资研究 · 数据驱动决策</p>
</div>
<div>
<div class="grid grid-flow-col gap-4">
<a class="btn btn-ghost btn-circle">
<i class="fab fa-twitter"></i>
</a>
<a class="btn btn-ghost btn-circle">
<i class="fab fa-github"></i>
</a>
<a class="btn btn-ghost btn-circle">
<i class="fab fa-linkedin"></i>
</a>
</div>
</div>
<div>
<p>Copyright © 2025 - 保留所有权利</p>
</div>
</footer>
<script>
// Smooth scroll for navigation links
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',
block: 'start'
});
}
});
});
// Add scroll effect to navbar
window.addEventListener('scroll', () => {
const navbar = document.querySelector('.navbar');
if (window.scrollY > 100) {
navbar.classList.add('shadow-xl');
} else {
navbar.classList.remove('shadow-xl');
}
});
// Animate elements 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 cards
document.querySelectorAll('.card').forEach(card => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
card.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
observer.observe(card);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,648 @@
# 谷歌概念深度解析:技术霸权与算力革命
<!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">
<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-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.hover-lift {
transition: all 0.3s ease;
}
.hover-lift:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.timeline-dot {
position: relative;
}
.timeline-dot::after {
content: '';
position: absolute;
width: 2px;
height: 100%;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
top: 100%;
left: 50%;
transform: translateX(-50%);
}
.timeline-dot:last-child::after {
display: none;
}
.pulse-animation {
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);
}
}
.scroll-smooth {
scroll-behavior: smooth;
}
.table-scroll {
overflow-x: auto;
}
.table-scroll::-webkit-scrollbar {
height: 8px;
}
.table-scroll::-webkit-scrollbar-track {
background: #f1f1f1;
}
.table-scroll::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.badge-glow {
animation: glow 2s ease-in-out infinite alternate;
}
@keyframes glow {
from {
box-shadow: 0 0 5px #667eea;
}
to {
box-shadow: 0 0 20px #667eea, 0 0 30px #667eea;
}
}
</style>
</head>
<body class="bg-gray-50 scroll-smooth">
<!-- Hero Section -->
<section class="relative bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 text-white overflow-hidden">
<div class="absolute inset-0 bg-black opacity-40"></div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<div class="text-center">
<div class="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full mb-6 pulse-animation">
<i class="fab fa-google text-3xl"></i>
</div>
<h1 class="text-5xl md:text-7xl font-bold mb-6">
<span class="gradient-text bg-gradient-to-r from-yellow-400 via-red-500 to-pink-500 text-transparent bg-clip-text">
谷歌概念
</span>
</h1>
<p class="text-xl md:text-2xl mb-8 text-gray-200">
AI技术霸权 × 算力基建革命 × Token经济范式
</p>
<div class="flex flex-wrap justify-center gap-4">
<span class="px-4 py-2 bg-green-500 bg-opacity-20 border border-green-400 rounded-full text-sm">
<i class="fas fa-rocket mr-2"></i>Gemini 3 Pro 登顶
</span>
<span class="px-4 py-2 bg-blue-500 bg-opacity-20 border border-blue-400 rounded-full text-sm">
<i class="fas fa-microchip mr-2"></i>930亿美金 CAPEX
</span>
<span class="px-4 py-2 bg-purple-500 bg-opacity-20 border border-purple-400 rounded-full text-sm">
<i class="fas fa-bolt mr-2"></i>480万亿 Token/月
</span>
</div>
</div>
</div>
<div class="absolute bottom-0 left-0 right-0">
<svg class="w-full h-20" viewBox="0 0 1440 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 120L60 110C120 100 240 80 360 70C480 60 600 60 720 65C840 70 960 80 1080 85C1200 90 1320 90 1380 90L1440 90V120H1380C1320 120 1200 120 1080 120C960 120 840 120 720 120C600 120 480 120 360 120C240 120 120 120 60 120H0V120Z" fill="#F9FAFB"/>
</svg>
</div>
</section>
<!-- 核心数据指标 -->
<section class="py-12 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-center mb-8 text-gray-800">核心数据指标</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
<div class="text-center p-6 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl hover-lift">
<div class="text-4xl font-bold text-purple-600 mb-2">1501</div>
<div class="text-gray-600">LMArena 评分</div>
</div>
<div class="text-center p-6 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl hover-lift">
<div class="text-4xl font-bold text-blue-600 mb-2">930亿</div>
<div class="text-gray-600">2025 CAPEX</div>
</div>
<div class="text-center p-6 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl hover-lift">
<div class="text-4xl font-bold text-green-600 mb-2">480万亿</div>
<div class="text-gray-600">月Token消耗</div>
</div>
<div class="text-center p-6 bg-gradient-to-br from-yellow-50 to-orange-50 rounded-xl hover-l">
<div class="text-4xl font-bold text-yellow-600 mb-2">15亿</div>
<div class="text-gray-600">AI搜索月活</div>
</div>
</div>
</div>
</section>
<!-- 事件时间轴 -->
<section class="py-16 bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-center mb-12 text-gray-800">概念事件时间轴</h2>
<div class="relative">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="timeline-dot">
<div class="bg-white p-6 rounded-xl shadow-lg hover-lift">
<div class="flex items-center mb-4">
<span class="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm font-semibold">2024年2月</span>
</div>
<h3 class="text-lg font-bold mb-2">产品发布</h3>
<p class="text-gray-600">推出ImageFX图像工具、Bard文生图功能基于Imagen 2模型</p>
</div>
</div>
<div class="timeline-dot">
<div class="bg-white p-6 rounded-xl shadow-lg hover-lift">
<div class="flex items-center mb-4">
<span class="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm font-semibold">2024年4月</span>
</div>
<h3 class="text-lg font-bold mb-2">战略发布</h3>
<p class="text-gray-600">Cloud Next大会发布Axion Arm CPU、TPU v5p</p>
</div>
</div>
<div class="timeline-dot">
<div class="bg-white p-6 rounded-xl shadow-lg hover-lift">
<div class="flex items-center mb-4">
<span class="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm font-semibold">2024年8月</span>
</div>
<h3 class="text-lg font-bold mb-2">产品发布</h3>
<p class="text-gray-600">Pixel 9系列Gemini Live语音助手端侧AI商业化</p>
</div>
</div>
<div class="timeline-dot">
<div class="bg-white p-6 rounded-xl shadow-lg hover-lift">
<div class="flex items-center mb-4">
<span class="px-3 py-1 bg-red-100 text-red-700 rounded-full text-sm font-semibold">2024年11月</span>
</div>
<h3 class="text-lg font-bold mb-2 badge-glow">技术突破</h3>
<p class="text-gray-600">Gemini 3 Pro登顶LMArena 1501分Nano Banana 2发布</p>
</div>
</div>
<div class="timeline-dot">
<div class="bg-white p-6 rounded-xl shadow-lg hover-lift">
<div class="flex items-center mb-4">
<span class="px-3 py-1 bg-yellow-100 text-yellow-700 rounded-full text-sm font-semibold">2025年1月</span>
</div>
<h3 class="text-lg font-bold mb-2">业绩验证</h3>
<p class="text-gray-600">Q1营收902亿美元资本开支172亿美元</p>
</div>
</div>
<div class="timeline-dot">
<div class="bg-white p-6 rounded-xl shadow-lg hover-lift">
<div class="flex items-center mb-4">
<span class="px-3 py-1 bg-indigo-100 text-indigo-700 rounded-full text-sm font-semibold">2025年10月</span>
</div>
<h3 class="text-lg font-bold mb-2">资本扩张</h3>
<p class="text-gray-600">CAPEX指引上调至910-930亿美元</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 核心逻辑 -->
<section class="py-16 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-center mb-12 text-gray-800">核心逻辑</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-gradient-to-br from-purple-600 to-purple-800 text-white p-8 rounded-2xl">
<div class="text-4xl mb-4">👑</div>
<h3 class="text-2xl font-bold mb-4">技术霸权确立</h3>
<ul class="space-y-2">
<li class="flex items-start">
<i class="fas fa-check-circle mt-1 mr-2 text-green-300"></i>
<span>Gemini 3 Pro LMArena 1501分历史最高</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle mt-1 mr-2 text-green-300"></i>
<span>Nano Banana 2图像生成顶尖</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle mt-1 mr-2 text-green-300"></i>
<span>TPU v7算力4610 TOPS持平H100</span>
</li>
</ul>
</div>
<div class="bg-gradient-to-br from-blue-600 to-blue-800 text-white p-8 rounded-2xl">
<div class="text-4xl mb-4">💰</div>
<h3 class="text-2xl font-bold mb-4">资本开支暴力扩张</h3>
<ul class="space-y-2">
<li class="flex items-start">
<i class="fas fa-check-circle mt-1 mr-2 text-green-300"></i>
<span>2025 CAPEX 910-930亿美元</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle mt-1 mr-2 text-green-300"></i>
<span>TPU电源单卡价值量1000+元</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle mt-1 mr-2 text-green-300"></i>
<span>2026年500万颗对应50亿市场</span>
</li>
</ul>
</div>
<div class="bg-gradient-to-br from-green-600 to-green-800 text-white p-8 rounded-2xl">
<div class="text-4xl mb-4">🚀</div>
<h3 class="text-2xl font-bold mb-4">商业化闭环验证</h3>
<ul class="space-y-2">
<li class="flex items-start">
<i class="fas fa-check-circle mt-1 mr-2 text-green-300"></i>
<span>月Token消耗480万亿</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle mt-1 mr-2 text-green-300"></i>
<span>Q3搜索增速创15%新高</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle mt-1 mr-2 text-green-300"></i>
<span>云业务积压订单1550亿美元</span>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 产业链图谱 -->
<section class="py-16 bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-center mb-12 text-gray-800">产业链图谱</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-xl font-bold mb-6 text-purple-600">上游(芯片/元器件)</h3>
<div class="space-y-3">
<div class="flex items-center p-3 bg-purple-50 rounded-lg">
<i class="fas fa-microchip text-purple-600 text-xl mr-3"></i>
<span>TPU芯片谷歌自研</span>
</div>
<div class="flex items-center p-3 bg-purple-50 rounded-lg">
<i class="fas fa-bolt text-purple-600 text-xl mr-3"></i>
<span>电源模块(新雷能、中富电路)</span>
</div>
<div class="flex items-center p-3 bg-purple-50 rounded-lg">
<i class="fas fa-network-wired text-purple-600 text-xl mr-3"></i>
<span>光器件(赛微电子、腾景科技)</span>
</div>
<div class="flex items-center p-3 bg-purple-50 rounded-lg">
<i class="fas fa-wind text-purple-600 text-xl mr-3"></i>
<span>液冷散热(英维克、博杰股份)</span>
</div>
</div>
</div>
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-xl font-bold mb-6 text-blue-600">下游(应用/服务)</h3>
<div class="space-y-3">
<div class="flex items-center p-3 bg-blue-50 rounded-lg">
<i class="fas fa-search text-blue-600 text-xl mr-3"></i>
<span>AI搜索15亿月活</span>
</div>
<div class="flex items-center p-3 bg-blue-50 rounded-lg">
<i class="fas fa-cloud text-blue-600 text-xl mr-3"></i>
<span>云计算GCP+Gemini API</span>
</div>
<div class="flex items-center p-3 bg-blue-50 rounded-lg">
<i class="fas fa-ad text-blue-600 text-xl mr-3"></i>
<span>AI营销易点天下、蓝色光标</span>
</div>
<div class="flex items-center p-3 bg-blue-50 rounded-lg">
<i class="fas fa-paint-brush text-blue-600 text-xl mr-3"></i>
<span>内容创作(万兴科技、视觉中国)</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 股票池表格 -->
<section class="py-16 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-center mb-12 text-gray-800">谷歌概念核心股票池</h2>
<div class="table-scroll">
<table class="w-full bg-white rounded-xl shadow-lg overflow-hidden">
<thead class="bg-gradient-to-r from-purple-600 to-blue-600 text-white">
<tr>
<th class="px-6 py-4 text-left">股票代码</th>
<th class="px-6 py-4 text-left">股票名称</th>
<th class="px-6 py-4 text-left">分类</th>
<th class="px-6 py-4 text-left">关联项目</th>
<th class="px-6 py-4 text-left">合作原因</th>
</tr>
</thead>
<tbody>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">300058.SZ</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">蓝色光标</span>
<span class="px-2 py-1 bg-purple-100 text-purple-700 rounded-full text-xs">核心</span>
</div>
</td>
<td class="px-6 py-4">AI营销</td>
<td class="px-6 py-4">谷歌合作伙伴</td>
<td class="px-6 py-4">通过运用谷歌营销产品实现出海价值</td>
</tr>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">301171.SZ</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">易点天下</span>
<span class="px-2 py-1 bg-green-100 text-green-700 rounded-full text-xs">高弹性</span>
</div>
</td>
<td class="px-6 py-4">AI营销</td>
<td class="px-6 py-4">Google一级代理</td>
<td class="px-6 py-4">Google广告在国内的一级代理提供H5广告变现方案</td>
</tr>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">300624.SZ</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">万兴科技</span>
<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded-full text-xs">应用</span>
</div>
</td>
<td class="px-6 py-4">AI应用</td>
<td class="px-6 py-4">接入谷歌Gemini</td>
<td class="px-6 py-4">已接入谷歌Gemini、Veo3、Nano banana等模型</td>
</tr>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">300456.SZ</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">赛微电子</span>
<span class="px-2 py-1 bg-purple-100 text-purple-700 rounded-full text-xs">核心</span>
</div>
</td>
<td class="px-6 py-4">OCS</td>
<td class="px-6 py-4">MEMS-OCS量产</td>
<td class="px-6 py-4">2023年并表后获谷歌批量采购订单</td>
</tr>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">300308.SZ</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">中际旭创</span>
<span class="px-2 py-1 bg-green-100 text-green-700 rounded-full text-xs">龙头</span>
</div>
</td>
<td class="px-6 py-4">光模块</td>
<td class="px-6 py-4">800G光模块</td>
<td class="px-6 py-4">2025年谷歌光模块采购量350万只占70%份额</td>
</tr>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">002463.SZ</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">沪电股份</span>
<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded-full text-xs">PCB</span>
</div>
</td>
<td class="px-6 py-4">PCB</td>
<td class="px-6 py-4">TPU PCB核心供应商</td>
<td class="px-6 py-4">谷歌TPU PCB核心供应商占TPU供应链约30%份额</td>
</tr>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">301183.SZ</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">东田微</span>
<span class="px-2 py-1 bg-purple-100 text-purple-700 rounded-full text-xs">核心</span>
</div>
</td>
<td class="px-6 py-4">OCS</td>
<td class="px-6 py-4">OCS光学方案</td>
<td class="px-6 py-4">为谷歌OCS光学引擎提供核心光学元件解决方案</td>
</tr>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">300676.SZ</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">华大九天</span>
<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded-full text-xs">EDA</span>
</div>
</td>
<td class="px-6 py-4">芯片设计</td>
<td class="px-6 py-4">EDA工具支持</td>
<td class="px-6 py-4">为谷歌TPU芯片设计提供EDA工具链支持</td>
</tr>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">603803.SH</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">瑞松科技</span>
<span class="px-2 py-1 bg-green-100 text-green-700 rounded-full text-xs">精密制造</span>
</div>
</td>
<td class="px-6 py-4">精密制造</td>
<td class="px-6 py-4">AI服务器制造</td>
<td class="px-6 py-4">参与谷歌AI服务器精密结构件制造</td>
</tr>
<tr class="border-b hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 font-semibold">301387.SZ</td>
<td class="px-6 py-4">
<div class="flex items-center">
<span class="mr-2">新雷能</span>
<span class="px-2 py-1 bg-purple-100 text-purple-700 rounded-full text-xs">电源</span>
</div>
</td>
<td class="px-6 py-4">电源</td>
<td class="px-6 py-4">TPU电源模块</td>
<td class="px-6 py-4">为谷歌TPU提供二次和三次电源模块单瓦价格比台系低20%</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 投资建议 -->
<section class="py-16 bg-gradient-to-br from-purple-50 to-blue-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-center mb-12 text-gray-800">投资建议与策略</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-bold mb-6 text-green-600">最具投资价值方向</h3>
<div class="space-y-4">
<div class="flex items-start">
<span class="flex-shrink-0 w-8 h-8 bg-green-100 text-green-600 rounded-full flex items-center justify-center font-bold mr-3">1</span>
<div>
<h4 class="font-semibold mb-1">TPU电源产业链最高优先级</h4>
<p class="text-gray-600 text-sm">电源是TPU扩产最大瓶颈26年50亿市场新雷能15亿收入弹性</p>
</div>
</div>
<div class="flex items-start">
<span class="flex-shrink-0 w-8 h-8 bg-green-100 text-green-600 rounded-full flex items-center justify-center font-bold mr-3">2</span>
<div>
<h4 class="font-semibold mb-1">OCS光交换产业链次高优先级</h4>
<p class="text-gray-600 text-sm">LightCounting预测2029年5万台15% CAGR国产供应商已获订单</p>
</div>
</div>
<div class="flex items-start">
<span class="flex-shrink-0 w-8 h-8 bg-green-100 text-green-600 rounded-full flex items-center justify-center font-bold mr-3">3</span>
<div>
<h4 class="font-semibold mb-1">AI搜索营销产业链中优先级</h4>
<p class="text-gray-600 text-sm">一级代理商受益于谷歌减少中间环节毛利率提升2-3pct</p>
</div>
</div>
</div>
</div>
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-bold mb-6 text-red-600">需规避的方向</h3>
<div class="space-y-4">
<div class="flex items-start">
<span class="flex-shrink-0 w-8 h-8 bg-red-100 text-red-600 rounded-full flex items-center justify-center font-bold mr-3">×</span>
<div>
<h4 class="font-semibold mb-1">纯模型接入方</h4>
<p class="text-gray-600 text-sm">Nano Banana中文生成不稳定API成本高毛利率承压</p>
</div>
</div>
<div class="flex items-start">
<span class="flex-shrink-0 w-8 h-8 bg-red-100 text-red-600 rounded-full flex items-center justify-center font-bold mr-3">×</span>
<div>
<h4 class="font-semibold mb-1">广告联盟依赖方</h4>
<p class="text-gray-600 text-sm">谷歌联盟广告持续萎缩,收入端受损</p>
</div>
</div>
<div class="flex items-start">
<span class="flex-shrink-0 w-8 h-8 bg-red-100 text-red-600 rounded-full flex items-center justify-center font-bold mr-3">×</span>
<div>
<h4 class="font-semibold mb-1">硬件代工</h4>
<p class="text-gray-600 text-sm">Pixel手机份额仅4.6%,硬件逻辑不纯</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 风险提示 -->
<section class="py-16 bg-gray-900 text-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-center mb-12">风险提示</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="glass-effect p-6 rounded-xl">
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl mb-4"></i>
<h3 class="text-xl font-bold mb-3">技术风险</h3>
<ul class="space-y-2 text-gray-300">
<li>• 与GPT-4o应用差距</li>
<li>• 多模态生成稳定性</li>
<li>• 算力瓶颈制约</li>
</ul>
</div>
<div class="glass-effect p-6 rounded-xl">
<i class="fas fa-chart-line text-red-400 text-3xl mb-4"></i>
<h3 class="text-xl font-bold mb-3">商业化风险</h3>
<ul class="space-y-2 text-gray-300">
<li>• 广告联盟萎缩</li>
<li>• 订阅变现不及预期</li>
<li>• Agent协议推广困难</li>
</ul>
</div>
<div class="glass-effect p-6 rounded-xl">
<i class="fas fa-gavel text-blue-400 text-3xl mb-4"></i>
<h3 class="text-xl font-bold mb-3">政策风险</h3>
<ul class="space-y-2 text-gray-300">
<li>• 反垄断最终判决</li>
<li>• OpenAI竞争冲击</li>
<li>• 中国应用适配不足</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-gray-800 text-white py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p class="text-gray-400">© 2024 谷歌概念深度解析 | 数据来源:公开研究报告、路演记录、新闻资讯</p>
<p class="text-gray-500 text-sm mt-2">投资有风险,入市需谨慎 | 本报告仅供参考,不构成投资建议</p>
</div>
</footer>
<script>
// 添加滚动动画
window.addEventListener('scroll', function() {
const elements = document.querySelectorAll('.hover-lift');
elements.forEach(element => {
const elementTop = element.getBoundingClientRect().top;
const elementBottom = element.getBoundingClientRect().bottom;
if (elementTop < window.innerHeight && elementBottom > 0) {
element.style.opacity = '1';
element.style.transform = 'translateY(0)';
}
});
});
// 初始化动画
document.addEventListener('DOMContentLoaded', function() {
const elements = document.querySelectorAll('.hover-lift');
elements.forEach(element => {
element.style.opacity = '0';
element.style.transform = 'translateY(20px)';
element.style.transition = 'all 0.6s ease';
});
});
// 表格行点击高亮
document.querySelectorAll('tbody tr').forEach(row => {
row.addEventListener('click', function() {
// 移除其他行的高亮
document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('bg-blue-50'));
// 添加当前行高亮
this.classList.add('bg-blue-50');
});
});
// 平滑滚动到锚点
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',
block: 'start'
});
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,824 @@
<!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 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%);
}
.glass-morphism {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.hover-lift {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.hover-lift:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
.timeline-item {
position: relative;
padding-left: 40px;
}
.timeline-item::before {
content: '';
position: absolute;
left: 0;
top: 8px;
width: 12px;
height: 12px;
background: #667eea;
border-radius: 50%;
}
.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;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-up {
animation: fadeInUp 0.6s ease-out;
}
.stock-table {
font-size: 12px;
}
.stock-table th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
position: sticky;
top: 0;
z-index: 10;
}
.stock-table tr:nth-child(even) {
background-color: #f9fafb;
}
.stock-table tr:hover {
background-color: #f3f4f6;
transform: scale(1.01);
transition: all 0.2s;
}
.badge-gradient {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.chain-arrow {
display: inline-block;
margin: 0 10px;
color: #9ca3af;
}
.floating {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
}
.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);
}
}
.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-50">
<!-- Hero Section -->
<div class="gradient-bg text-white py-20 px-4">
<div class="max-w-7xl mx-auto">
<div class="text-center fade-in-up">
<h1 class="text-5xl md:text-6xl font-bold mb-4 floating">
<i class="fas fa-rocket mr-4"></i>远程火力
</h1>
<p class="text-xl md:text-2xl opacity-90 mb-8">现代陆军第四代核心骨干装备 · 高效费比战争撒手锏</p>
<div class="flex flex-wrap justify-center gap-4">
<span class="glass-morphism px-4 py-2 rounded-full">
<i class="fas fa-crosshairs mr-2"></i>射程 70-500km
</span>
<span class="glass-morphism px-4 py-2 rounded-full">
<i class="fas fa-bullseye mr-2"></i>CEP精度 30m
</span>
<span class="glass-morphism px-4 py-2 rounded-full">
<i class="fas fa-dollar-sign mr-2"></i>成本仅为导弹1/10
</span>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 py-12">
<!-- 概念定义与背景 -->
<section class="mb-12 fade-in-up">
<div class="bg-white rounded-2xl shadow-xl p-8 hover-lift">
<h2 class="text-3xl font-bold mb-6 text-gray-800">
<i class="fas fa-info-circle text-purple-600 mr-3"></i>概念定义与背景
</h2>
<div class="prose max-w-none text-gray-600">
<p class="text-lg leading-relaxed mb-4">
<strong>远程火力</strong>(简称"远火")在军事领域特指远程火箭炮武器系统,是现代陆军第四代核心骨干装备。该系统通过远程火箭弹实现<strong>70-500公里射程</strong>的精确打击能力有效填补传统身管火炮20-50公里与战术弹道导弹千公里级之间的火力空白。
</p>
<div class="grid md:grid-cols-3 gap-6 mt-6">
<div class="bg-gradient-to-br from-blue-50 to-purple-50 p-6 rounded-xl">
<h3 class="font-bold text-lg mb-3 text-blue-800"><i class="fas fa-bolt mr-2"></i>核心特征</h3>
<ul class="space-y-2 text-sm">
<li>• 低成本高效费比</li>
<li>• 模块化设计</li>
<li>• 精准制导能力</li>
<li>• 快速部署机动</li>
</ul>
</div>
<div class="bg-gradient-to-br from-green-50 to-teal-50 p-6 rounded-xl">
<h3 class="font-bold text-lg mb-3 text-green-800"><i class="fas fa-shield-alt mr-2"></i>战略地位</h3>
<ul class="space-y-2 text-sm">
<li>• 填补火力空白</li>
<li>• 战争消耗主力</li>
<li>• 精打要害利器</li>
<li>• 破击体系关键</li>
</ul>
</div>
<div class="bg-gradient-to-br from-orange-50 to-red-50 p-6 rounded-xl">
<h3 class="font-bold text-lg mb-3 text-orange-800"><i class="fas fa-globe mr-2"></i>国际形势</h3>
<ul class="space-y-2 text-sm">
<li>• 美军扩编500套</li>
<li>• 欧盟2030战备计划</li>
<li>• 俄乌冲突验证价值</li>
<li>• 全球军贸需求爆发</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- 核心观点 -->
<section class="mb-12 fade-in-up">
<div class="bg-white rounded-2xl shadow-xl p-8 hover-lift">
<h2 class="text-3xl font-bold mb-6 text-gray-800">
<i class="fas fa-lightbulb text-yellow-500 mr-3"></i>核心观点
</h2>
<div class="grid md:grid-cols-2 gap-6">
<div class="border-l-4 border-purple-500 pl-6">
<h3 class="font-bold text-lg mb-3">三大驱动力共振</h3>
<ul class="space-y-3 text-gray-600">
<li class="flex items-start">
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
<span><strong>军事战略重构</strong>美军将其与高超音速导弹并列为优先事项预算300亿美元</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
<span><strong>技术成熟度突破</strong>模块化装填效率提升6倍精度达30米级</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
<span><strong>实战验证需求</strong>单套海马斯消耗500-625枚弹药储备严重不足</span>
</li>
</ul>
</div>
<div class="border-l-4 border-blue-500 pl-6">
<h3 class="font-bold text-lg mb-3">三大预期差</h3>
<ul class="space-y-3 text-gray-600">
<li class="flex items-start">
<i class="fas fa-exclamation-triangle text-yellow-500 mt-1 mr-2"></i>
<span><strong>市场空间VS订单节奏</strong>:研报"万台"预期 vs 实际年产20套</span>
</li>
<li class="flex items-start">
<i class="fas fa-exclamation-triangle text-yellow-500 mt-1 mr-2"></i>
<span><strong>垄断VS竞争</strong>:北方导航"独供"仅限大口径型号</span>
</li>
<li class="flex items-start">
<i class="fas fa-exclamation-triangle text-yellow-500 mt-1 mr-2"></i>
<span><strong>成本优势VS价格风险</strong>:军采阶梯降价或冲击毛利率</span>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 催化事件时间轴 -->
<section class="mb-12 fade-in-up">
<div class="bg-white rounded-2xl shadow-xl p-8 hover-lift">
<h2 class="text-3xl font-bold mb-6 text-gray-800">
<i class="fas fa-timeline text-indigo-600 mr-3"></i>催化事件时间轴
</h2>
<div class="space-y-6">
<div class="timeline-item">
<div class="bg-gradient-to-r from-purple-50 to-pink-50 p-4 rounded-lg">
<span class="text-sm font-semibold text-purple-600">2024年7月-10月</span>
<h3 class="font-bold mt-1">研究机构密集发布深度研报</h3>
<p class="text-gray-600 text-sm mt-2">长江军工、东兴证券等7-10份报告系统构建分析框架将远火定位为新时期陆军"战争之神"</p>
</div>
</div>
<div class="timeline-item">
<div class="bg-gradient-to-r from-blue-50 to-cyan-50 p-4 rounded-lg">
<span class="text-sm font-semibold text-blue-600">2024年9月</span>
<h3 class="font-bold mt-1">美军扩编计划曝光</h3>
<p class="text-gray-600 text-sm mt-2">将远程火力战略地位提升至与高超音速导弹同级采购量从105套增至500套</p>
</div>
</div>
<div class="timeline-item">
<div class="bg-gradient-to-r from-green-50 to-emerald-50 p-4 rounded-lg">
<span class="text-sm font-semibold text-green-600">2025年2月</span>
<h3 class="font-bold mt-1">技术路线明确</h3>
<p class="text-gray-600 text-sm mt-2">确立固体火箭发动机、多脉冲发动机、碳纤维壳体为技术核心</p>
</div>
</div>
<div class="timeline-item">
<div class="bg-gradient-to-r from-orange-50 to-red-50 p-4 rounded-lg">
<span class="text-sm font-semibold text-orange-600">2025年5月</span>
<h3 class="font-bold mt-1">军贸订单实证</h3>
<p class="text-gray-600 text-sm mt-2">巴基斯坦采购中国远程火箭弹及自行加榴炮,获得实战验证</p>
</div>
</div>
</div>
</div>
</section>
<!-- 产业链图谱 -->
<section class="mb-12 fade-in-up">
<div class="bg-white rounded-2xl shadow-xl p-8 hover-lift">
<h2 class="text-3xl font-bold mb-6 text-gray-800">
<i class="fas fa-sitemap text-teal-600 mr-3"></i>产业链图谱
</h2>
<div class="overflow-x-auto">
<div class="min-w-max">
<div class="flex items-center justify-between mb-4">
<div class="text-center bg-gray-100 p-3 rounded-lg">
<h3 class="font-bold">上游材料及元器件</h3>
<p class="text-sm text-gray-600">价值占比15-20%</p>
</div>
<i class="fas fa-arrow-right text-2xl text-gray-400"></i>
<div class="text-center bg-purple-100 p-3 rounded-lg">
<h3 class="font-bold">中游核心系统</h3>
<p class="text-sm text-gray-600">价值占比60-70%</p>
<span class="badge-gradient text-white text-xs px-2 py-1 rounded-full">战略制高点</span>
</div>
<i class="fas fa-arrow-right text-2xl text-gray-400"></i>
<div class="text-center bg-gray-100 p-3 rounded-lg">
<h3 class="font-bold">下游总装及平台</h3>
<p class="text-sm text-gray-600">价值占比15-20%</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div class="space-y-2">
<div class="flex items-center"><span class="w-2 h-2 bg-blue-500 rounded-full mr-2"></span>碳纤维:光威复材</div>
<div class="flex items-center"><span class="w-2 h-2 bg-blue-500 rounded-full mr-2"></span>MLCC电容鸿远电子</div>
<div class="flex items-center"><span class="w-2 h-2 bg-blue-500 rounded-full mr-2"></span>连接器:航天电器</div>
<div class="flex items-center"><span class="w-2 h-2 bg-blue-500 rounded-full mr-2"></span>火工品:北化股份</div>
</div>
<div class="space-y-2">
<div class="flex items-center"><span class="w-2 h-2 bg-purple-500 rounded-full mr-2"></span>制导系统:北方导航</div>
<div class="flex items-center"><span class="w-2 h-2 bg-purple-500 rounded-full mr-2"></span>动力模块:国科军工</div>
<div class="flex items-center"><span class="w-2 h-2 bg-purple-500 rounded-full mr-2"></span>光纤环:长盈通</div>
</div>
<div class="space-y-2">
<div class="flex items-center"><span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>火箭炮总装:中兵红箭</div>
<div class="flex items-center"><span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>系统总体:航天彩虹</div>
<div class="flex items-center"><span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>外贸出口:北方工业</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 核心公司对比 -->
<section class="mb-12 fade-in-up">
<div class="bg-white rounded-2xl shadow-xl p-8 hover-lift">
<h2 class="text-3xl font-bold mb-6 text-gray-800">
<i class="fas fa-crown text-yellow-500 mr-3"></i>核心公司对比分析
</h2>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gradient-to-r from-purple-600 to-blue-600 text-white">
<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-center">投资评级</th>
</tr>
</thead>
<tbody>
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-bold">北方导航</td>
<td class="px-4 py-3">制导舱(中段+末段)</td>
<td class="px-4 py-3">独供制导舱280/500km射程核心供应商</td>
<td class="px-4 py-3">订单节奏不确定,价格承压</td>
<td class="px-4 py-3 text-center"><span class="bg-green-100 text-green-800 px-3 py-1 rounded-full">超配</span></td>
</tr>
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-bold">国科军工</td>
<td class="px-4 py-3">动力模块(固体发动机)</td>
<td class="px-4 py-3">稀缺军工牌照弹载动力模块CAGR 35.56%</td>
<td class="px-4 py-3">产能扩张慢,军品交付不及预期</td>
<td class="px-4 py-3 text-center"><span class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full">标配</span></td>
</tr>
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-bold">长盈通</td>
<td class="px-4 py-3">光纤环(陀螺仪核心)</td>
<td class="px-4 py-3">军用光纤环龙头军民两用占比90%</td>
<td class="px-4 py-3">MEMS技术替代风险民品波动</td>
<td class="px-4 py-3 text-center"><span class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full">标配</span></td>
</tr>
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-bold">中兵红箭</td>
<td class="px-4 py-3">整弹总装</td>
<td class="px-4 py-3">兵器集团唯一弹药平台特种装备占比50%+</td>
<td class="px-4 py-3">传统弹药拖累估值,转型进度慢</td>
<td class="px-4 py-3 text-center"><span class="bg-yellow-100 text-yellow-800 px-3 py-1 rounded-full">观望</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 股票数据表格 -->
<section class="mb-12 fade-in-up">
<div class="bg-white rounded-2xl shadow-xl p-8 hover-lift">
<h2 class="text-3xl font-bold mb-6 text-gray-800">
<i class="fas fa-table text-indigo-600 mr-3"></i>远程火力概念股票全览
</h2>
<div class="overflow-x-auto">
<table class="stock-table w-full">
<thead>
<tr>
<th class="px-2 py-2 text-left">股票名称</th>
<th class="px-2 py-2 text-left">分类</th>
<th class="px-2 py-2 text-left">产业链</th>
<th class="px-2 py-2 text-left">项目</th>
<th class="px-2 py-2 text-left">关联逻辑</th>
</tr>
</thead>
<tbody>
<tr>
<td class="px-2 py-2 font-semibold">航天电子</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">总装</td>
<td class="px-2 py-2">精确制导导弹</td>
<td class="px-2 py-2">稀缺主机厂</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">中兵红箭</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">总装</td>
<td class="px-2 py-2">弹药总装</td>
<td class="px-2 py-2">唯一弹药平台</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">航天彩虹</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">总装</td>
<td class="px-2 py-2">多用途模块化导弹</td>
<td class="px-2 py-2">填补国内空白</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">洪都航空</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">总装</td>
<td class="px-2 py-2">导弹业务</td>
<td class="px-2 py-2">厂所合一平台</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">高德红外</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">导引头</td>
<td class="px-2 py-2">多款型号产品</td>
<td class="px-2 py-2">精确制导优势</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">菲利华</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">材料</td>
<td class="px-2 py-2">军工配套</td>
<td class="px-2 py-2">军工蓝宝石球罩</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">铂力特</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">金属3D打印</td>
<td class="px-2 py-2">3D打印零件</td>
<td class="px-2 py-2">导弹型号应用</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">派克新材</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">锻造</td>
<td class="px-2 py-2">导弹配套</td>
<td class="px-2 py-2">型号研制配套</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">航宇科技</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">锻造</td>
<td class="px-2 py-2">导弹配套</td>
<td class="px-2 py-2">锻件应用</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">钢研高纳</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">材料</td>
<td class="px-2 py-2">高温合金</td>
<td class="px-2 py-2">导弹应用</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">光威复材</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">材料</td>
<td class="px-2 py-2">碳纤维</td>
<td class="px-2 py-2">发动机壳体</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">抚顺特钢</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">材料</td>
<td class="px-2 py-2">高强度钢</td>
<td class="px-2 py-2">关键材料</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">甘化科工</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">钨合金</td>
<td class="px-2 py-2">钨合金预制破片</td>
<td class="px-2 py-2">导弹配套</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">国泰集团</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">钨合金</td>
<td class="px-2 py-2">军用钨基材料</td>
<td class="px-2 py-2">毁伤材料</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">北方长龙</td>
<td class="px-2 py-2">导弹</td>
<td class="px-2 py-2">其他</td>
<td class="px-2 py-2">弹药装备</td>
<td class="px-2 py-2">复合材料弹药箱</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">北化股份</td>
<td class="px-2 py-2">战斗部系统</td>
<td class="px-2 py-2">发射药</td>
<td class="px-2 py-2">硝化棉</td>
<td class="px-2 py-2">硝化棉龙头</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">长城军工</td>
<td class="px-2 py-2">战斗部系统</td>
<td class="px-2 py-2">弹药</td>
<td class="px-2 py-2">传统弹药</td>
<td class="px-2 py-2">老牌弹药公司</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">新余国科</td>
<td class="px-2 py-2">战斗部系统</td>
<td class="px-2 py-2">火工品</td>
<td class="px-2 py-2">军用火工品</td>
<td class="px-2 py-2">火工元件</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">国科军工</td>
<td class="px-2 py-2">战斗部系统</td>
<td class="px-2 py-2">火工品</td>
<td class="px-2 py-2">导弹配套</td>
<td class="px-2 py-2">二三级配套</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">北方导航</td>
<td class="px-2 py-2">惯性制导(中段)</td>
<td class="px-2 py-2">导航</td>
<td class="px-2 py-2">惯性导航系统</td>
<td class="px-2 py-2">成本竞争优势</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">星网宇达</td>
<td class="px-2 py-2">惯性制导(中段)</td>
<td class="px-2 py-2">导航</td>
<td class="px-2 py-2">惯性导航系统</td>
<td class="px-2 py-2">远程制导炸弹</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">理工导航</td>
<td class="px-2 py-2">惯性制导(中段)</td>
<td class="px-2 py-2">导航</td>
<td class="px-2 py-2">惯性导航系统</td>
<td class="px-2 py-2">制导炸弹配套</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">长盈通</td>
<td class="px-2 py-2">卫星制导(中段)</td>
<td class="px-2 py-2">光纤</td>
<td class="px-2 py-2">军用光纤陀螺</td>
<td class="px-2 py-2">光纤环供应商</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">芯动联科</td>
<td class="px-2 py-2">卫星制导(中段)</td>
<td class="px-2 py-2">MEMS惯性传感器</td>
<td class="px-2 py-2">MEMS惯性传感器</td>
<td class="px-2 py-2">高性能传感器</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">盟升电子</td>
<td class="px-2 py-2">卫星制导(中段)</td>
<td class="px-2 py-2">卫星导航</td>
<td class="px-2 py-2">卫星导航系统</td>
<td class="px-2 py-2">导航系统</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">振芯科技</td>
<td class="px-2 py-2">卫星制导(中段)</td>
<td class="px-2 py-2">卫星导航</td>
<td class="px-2 py-2">卫星导航系统</td>
<td class="px-2 py-2">导航系统</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">亚光科技</td>
<td class="px-2 py-2">卫星制导(中段)</td>
<td class="px-2 py-2">微波电子</td>
<td class="px-2 py-2">微波电子元器件</td>
<td class="px-2 py-2">导弹导引头</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">天箭科技</td>
<td class="px-2 py-2">制导系统</td>
<td class="px-2 py-2">微波/毫米波</td>
<td class="px-2 py-2">微波固态发射机</td>
<td class="px-2 py-2">推进稀布阵</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">雷电微力</td>
<td class="px-2 py-2">制导系统</td>
<td class="px-2 py-2">微波/毫米波</td>
<td class="px-2 py-2">毫米波有源相控阵</td>
<td class="px-2 py-2">相控阵</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">臻镭科技</td>
<td class="px-2 py-2">制导系统</td>
<td class="px-2 py-2">微波/毫米波</td>
<td class="px-2 py-2">射频芯片</td>
<td class="px-2 py-2">射频前端</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">国博电子</td>
<td class="px-2 py-2">制导系统</td>
<td class="px-2 py-2">微波/毫米波</td>
<td class="px-2 py-2">射频模块</td>
<td class="px-2 py-2">射频模块</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">高德红外</td>
<td class="px-2 py-2">红外制导(末段)</td>
<td class="px-2 py-2">红外</td>
<td class="px-2 py-2">制冷探测器</td>
<td class="px-2 py-2">批量应用</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">睿创微纳</td>
<td class="px-2 py-2">红外制导(末段)</td>
<td class="px-2 py-2">红外</td>
<td class="px-2 py-2">非制冷红外热成像</td>
<td class="px-2 py-2">MEMS芯片</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">光谱股份</td>
<td class="px-2 py-2">光学制导(末段)</td>
<td class="px-2 py-2">光学</td>
<td class="px-2 py-2">光学制导</td>
<td class="px-2 py-2">导引头</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">新光光电</td>
<td class="px-2 py-2">光学制导(末段)</td>
<td class="px-2 py-2">光学</td>
<td class="px-2 py-2">光学制导</td>
<td class="px-2 py-2">导弹型号</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">智明达</td>
<td class="px-2 py-2">嵌入式计算机</td>
<td class="px-2 py-2">计算机</td>
<td class="px-2 py-2">嵌入式计算机</td>
<td class="px-2 py-2">弹载应用</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">雷科防务</td>
<td class="px-2 py-2">嵌入式计算机</td>
<td class="px-2 py-2">计算机</td>
<td class="px-2 py-2">嵌入式计算机</td>
<td class="px-2 py-2">国防应用</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">鸿远电子</td>
<td class="px-2 py-2">元器件及部件</td>
<td class="px-2 py-2">电容</td>
<td class="px-2 py-2">军用MLCC</td>
<td class="px-2 py-2">高可靠MLCC</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">宏达电子</td>
<td class="px-2 py-2">元器件及部件</td>
<td class="px-2 py-2">电容</td>
<td class="px-2 py-2">军用钽电容器</td>
<td class="px-2 py-2">钽电容龙头</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">航天电器</td>
<td class="px-2 py-2">元器件及部件</td>
<td class="px-2 py-2">连接器</td>
<td class="px-2 py-2">高端连接器</td>
<td class="px-2 py-2">导弹配套</td>
</tr>
<tr>
<td class="px-2 py-2 font-semibold">中航光电</td>
<td class="px-2 py-2">元器件及部件</td>
<td class="px-2 py-2">连接器</td>
<td class="px-2 py-2">连接器</td>
<td class="px-2 py-2">导弹应用</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 风险提示 -->
<section class="mb-12 fade-in-up">
<div class="bg-white rounded-2xl shadow-xl p-8 hover-lift">
<h2 class="text-3xl font-bold mb-6 text-gray-800">
<i class="fas fa-exclamation-triangle text-red-500 mr-3"></i>风险提示
</h2>
<div class="grid md:grid-cols-3 gap-6">
<div class="risk-high p-4 rounded-lg">
<h3 class="font-bold text-lg mb-3 text-red-700">技术风险</h3>
<ul class="space-y-2 text-sm text-gray-600">
<li>• MEMS陀螺仪军品认证未通过</li>
<li>• 卫星导航抗干扰能力存疑</li>
<li>• 高端制导器件仍有卡脖子环节</li>
</ul>
</div>
<div class="risk-medium p-4 rounded-lg">
<h3 class="font-bold text-lg mb-3 text-yellow-700">商业化风险</h3>
<ul class="space-y-2 text-sm text-gray-600">
<li>• 军采阶梯降价冲击毛利</li>
<li>• 订单能见度极低</li>
<li>• 订单空窗期可能长达18个月</li>
</ul>
</div>
<div class="risk-low p-4 rounded-lg">
<h3 class="font-bold text-lg mb-3 text-green-700">政策风险</h3>
<ul class="space-y-2 text-sm text-gray-600">
<li>• 军贸审批不确定性</li>
<li>• 军工央企内部竞争</li>
<li>• 和平谈判导致需求降温</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 投资启示 -->
<section class="mb-12 fade-in-up">
<div class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-2xl shadow-xl p-8">
<h2 class="text-3xl font-bold mb-6">
<i class="fas fa-chart-line mr-3"></i>投资启示
</h2>
<div class="grid md:grid-cols-2 gap-8">
<div>
<h3 class="text-xl font-bold mb-4">概念阶段判断</h3>
<p class="mb-4">远程火力正处于从<strong>主题炒作向基本面驱动</strong>过渡的"阵痛期",长期逻辑坚实但短期面临验证挑战。</p>
<div class="bg-white/10 backdrop-blur p-4 rounded-lg">
<p class="text-sm">当前市场过度关注宏大叙事忽视微观订单。建议降低仓位至标配待Q3催化剂落地后再决策。</p>
</div>
</div>
<div>
<h3 class="text-xl font-bold mb-4">配置建议</h3>
<ul class="space-y-3">
<li class="flex items-center">
<i class="fas fa-arrow-up text-green-400 mr-3"></i>
<span><strong>超配:</strong>北方导航(制导系统龙头)</span>
</li>
<li class="flex items-center">
<i class="fas fa-minus text-yellow-400 mr-3"></i>
<span><strong>标配:</strong>国科军工、长盈通</span>
</li>
<li class="flex items-center">
<i class="fas fa-arrow-down text-red-400 mr-3"></i>
<span><strong>规避:</strong>间接受益标的(航亚科技等)</span>
</li>
</ul>
</div>
</div>
<div class="mt-8 text-center">
<div class="inline-flex items-center bg-white/20 backdrop-blur px-6 py-3 rounded-full">
<span class="pulse-dot w-3 h-3 bg-green-400 rounded-full mr-3"></span>
<span class="font-semibold">一句话总结:值得长期跟踪,但当前处于"证伪窗口期",不见订单不撒鹰</span>
</div>
</div>
</div>
</section>
</div>
<!-- Footer -->
<footer class="bg-gray-800 text-white py-8">
<div class="max-w-7xl mx-auto px-4 text-center">
<p class="text-sm opacity-75">本页面信息仅供参考,不构成投资建议</p>
<p class="text-xs mt-2 opacity-50">数据来源:研报、路演、公告等公开信息</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.classList.add('fade-in-up');
}
});
}, observerOptions);
document.querySelectorAll('section').forEach(section => {
observer.observe(section);
});
// 表格行高亮
document.querySelectorAll('.stock-table tr').forEach(row => {
row.addEventListener('mouseenter', function() {
this.style.transform = 'scale(1.01)';
});
row.addEventListener('mouseleave', function() {
this.style.transform = 'scale(1)';
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,643 @@
<!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>阿里AI千问、灵光 - 概念深度分析</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.timeline-dot::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background: #3b82f6;
top: 50%;
left: -8px;
transform: translateY(-50%);
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
}
.float-animation {
animation: float 3s ease-in-out infinite;
}
.glass-effect {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
@media (max-width: 640px) {
.table-responsive {
font-size: 12px;
}
.table-responsive th,
.table-responsive td {
padding: 4px;
}
}
</style>
</head>
<body class="bg-base-300 text-base-content">
<!-- Hero Section -->
<section class="hero min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900">
<div class="hero-content text-center text-neutral-content">
<div class="max-w-4xl">
<h1 class="text-6xl font-bold mb-4 gradient-text">阿里AI千问、灵光</h1>
<p class="text-xl mb-8 opacity-90">从B端技术输出到C端生态入口的历史性拐点</p>
<div class="stats shadow-lg glass-effect">
<div class="stat">
<div class="stat-title">下载量</div>
<div class="stat-value text-primary">1000万+</div>
<div class="stat-desc">千问APP公测一周</div>
</div>
<div class="stat">
<div class="stat-title">App Store排名</div>
<div class="stat-value text-secondary">Top 3</div>
<div class="stat-desc">2天冲榜</div>
</div>
<div class="stat">
<div class="stat-title">开源模型</div>
<div class="stat-value">200+</div>
<div class="stat-desc">全球下载量超3亿次</div>
</div>
</div>
</div>
</div>
</section>
<!-- Core Insights -->
<section class="py-16 bg-base-200">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
<i class="ri-lightbulb-flash-line text-yellow-500"></i> 核心观点摘要
</h2>
<div class="alert alert-warning glass-effect shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<div>
<h3 class="font-bold text-lg">核心观点</h3>
<div class="text-sm mt-2">阿里AI战略正经历从B端技术输出到C端生态入口的历史性拐点千问APP的爆发式增长验证了其"开源技术+场景垄断+免费策略"的独特路径有效性。然而当前市场高度聚焦于用户数据和下载量却可能忽视两个关键预期差一是AI收入占比仍处个位数商业化路径尚未跑通二是技术层面虽在开源生态占据第一但推理能力与DeepSeek等竞品仍存在差距。</div>
</div>
</div>
</div>
</section>
<!-- Timeline -->
<section class="py-16 bg-base-300">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
<i class="ri-time-line text-blue-500"></i> 关键时间轴
</h2>
<div class="relative">
<div class="absolute left-8 top-0 bottom-0 w-0.5 bg-base-content/20"></div>
<!-- 2024年4月 -->
<div class="mb-8 ml-16 relative timeline-dot">
<div class="timeline-date text-primary font-bold text-lg">2024年4月</div>
<div class="card glass-effect shadow-xl">
<div class="card-body">
<h3 class="card-title">技术基建期</h3>
<ul class="list-disc list-inside space-y-2 text-sm">
<li>阿里云发布通义千问2.5性能追平GPT-4 Turbo</li>
<li>Qwen2.5系列开源,奠定全球第一开源模型族地位</li>
<li>衍生模型超10万下载量突破700万次</li>
</ul>
</div>
</div>
</div>
<!-- 2025年4月 -->
<div class="mb-8 ml-16 relative timeline-dot">
<div class="timeline-date text-primary font-bold text-lg">2025年4月</div>
<div class="card glass-effect shadow-xl">
<div class="card-body">
<h3 class="card-title">Qwen3重磅发布</h3>
<ul class="list-disc list-inside space-y-2 text-sm">
<li>Qwen3-235B-A22B在编程、数学基准测试中超越DeepSeek-R1、GPT-4.1</li>
<li>登顶Hugging Face趋势榜</li>
<li>Qwen3 Coder编程能力达全球SOTAAPI调用量突破千亿级Tokens</li>
</ul>
</div>
</div>
</div>
<!-- 2025年11月 -->
<div class="mb-8 ml-16 relative timeline-dot">
<div class="timeline-date text-error font-bold text-lg">2025年11月 - C端产品爆发期</div>
<div class="card glass-effect shadow-xl">
<div class="card-body">
<h3 class="card-title">里程碑时刻</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div class="badge badge-info p-4">
<div class="font-bold">11月14日</div>
<div>通义App升级为千问App</div>
</div>
<div class="badge badge-success p-4">
<div class="font-bold">11月17日</div>
<div>千问App公测版上线</div>
<div>2天冲榜Top 3</div>
</div>
<div class="badge badge-warning p-4">
<div class="font-bold">11月18日</div>
<div>灵光App发布</div>
<div>4天下载破100万</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Core Logic -->
<section class="py-16 bg-base-200">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
<i class="ri-mind-map text-purple-500"></i> 核心驱动力分析
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- 技术开源 -->
<div class="card glass-effect shadow-xl hover:shadow-2xl transition-all">
<div class="card-body">
<div class="card-title flex items-center gap-2">
<i class="ri-code-s-slash-line text-2xl text-blue-500"></i>
<span>技术开源战略</span>
<div class="badge badge-info">★★★★☆</div>
</div>
<div class="text-sm space-y-2">
<p>开源200+模型全球下载量超3亿次</p>
<p>衍生模型数量超10万稳居Hugging Face第一</p>
<p>区域性语言处理优势(印尼语、泰语)</p>
<p class="text-warning">短板推理能力与DeepSeek存在差距</p>
</div>
</div>
</div>
<!-- 生态整合 -->
<div class="card glass-effect shadow-xl hover:shadow-2xl transition-all">
<div class="card-body">
<div class="card-title flex items-center gap-2">
<i class="ri-landscape-line text-2xl text-green-500"></i>
<span>C端生态整合</span>
<div class="badge badge-success">★★★★★</div>
</div>
<div class="text-sm space-y-2">
<p>淘宝、高德、支付宝、飞猪全场景覆盖</p>
<p>从聊天到办事的能力闭环</p>
<p>石基信息打通酒店预订系统</p>
<p class="text-success">护城河:竞品无法复制的场景垄断</p>
</div>
</div>
</div>
<!-- 战略投入 -->
<div class="card glass-effect shadow-xl hover:shadow-2xl transition-all">
<div class="card-body">
<div class="card-title flex items-center gap-2">
<i class="ri-rocket-line text-2xl text-red-500"></i>
<span>战略资源投入</span>
<div class="badge badge-error">★★★★★</div>
</div>
<div class="text-sm space-y-2">
<p>3800亿AI基础设施投入三年</p>
<p>吴泳铭定义"AI时代的未来之战"</p>
<p>资本开支2025年预计增长超50%</p>
<p class="text-error">风险ROI压力巨大</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Market Sentiment -->
<section class="py-16 bg-base-300">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
<i class="ri-bar-chart-line text-yellow-500"></i> 市场热度与预期差
</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="card glass-effect shadow-xl">
<div class="card-body">
<h3 class="card-title">
<i class="ri-fire-line text-orange-500"></i> 市场热度
</h3>
<div class="space-y-3 text-sm">
<div class="flex justify-between items-center">
<span>研报覆盖密度</span>
<div class="badge badge-warning">峰值</div>
</div>
<div class="flex justify-between items-center">
<span>概念板块涨幅</span>
<progress class="progress progress-warning w-56" value="85" max="100"></progress>
</div>
<div class="flex justify-between items-center">
<span>乐观情绪占比</span>
<progress class="progress progress-success w-56" value="90" max="100"></progress>
</div>
</div>
</div>
</div>
<div class="card glass-effect shadow-xl">
<div class="card-body">
<h3 class="card-title">
<i class="ri-alert-line text-red-500"></i> 关键预期差
</h3>
<div class="space-y-3 text-sm">
<div class="alert alert-error">
<i class="ri-money-dollar-circle-line"></i>
<div>C端爆发 ≠ 商业化兑现</div>
<div class="text-xs">AI收入占比仍处个位数</div>
</div>
<div class="alert alert-warning">
<i class="ri-git-branch-line"></i>
<div>开源第一 ≠ 性能无敌</div>
<div class="text-xs">Agent能力低于DeepSeek</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Industry Chain -->
<section class="py-16 bg-base-200">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
<i class="ri-links-line text-indigo-500"></i> 产业链图谱
</h2>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 上游 -->
<div class="card glass-effect shadow-xl">
<div class="card-body">
<h3 class="card-title text-primary">上游:算力基础设施</h3>
<div class="text-sm space-y-2">
<p class="font-bold">价值占比: 40%</p>
<ul class="list-disc list-inside">
<li>IDC/智算中心: 数据港、杭钢股份、科华数据</li>
<li>AI芯片: 寒武纪、海光信息、平头哥</li>
<li>服务器: 浪潮信息、中科曙光、工业富联</li>
<li>光模块: 中际旭创、新易盛</li>
</ul>
</div>
</div>
</div>
<!-- 中游 -->
<div class="card glass-effect shadow-xl">
<div class="card-body">
<h3 class="card-title text-secondary">中游:模型与平台</h3>
<div class="text-sm space-y-2">
<p class="font-bold">价值占比: 30%(阿里主导)</p>
<ul class="list-disc list-inside">
<li>大模型: 通义千问已开源200+模型)</li>
<li>MaaS平台: 百炼29万企业用户</li>
<li>工具链: 通义灵码(已开始收费)</li>
</ul>
</div>
</div>
</div>
<!-- 下游 -->
<div class="card glass-effect shadow-xl">
<div class="card-body">
<h3 class="card-title text-success">下游:应用与场景</h3>
<div class="text-sm space-y-2">
<p class="font-bold">价值占比: 30%</p>
<ul class="list-disc list-inside">
<li>电商/消费: 光云科技、丽人丽妆</li>
<li>旅游/酒店: 石基信息(核心标的)</li>
<li>交通/物流: 千方科技</li>
<li>数据服务: 值得买</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Stock Tables -->
<section class="py-16 bg-base-300">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
<i class="ri-stock-line text-green-500"></i> 核心关联股票
</h2>
<!-- Table 1 -->
<div class="mb-12">
<h3 class="text-2xl font-bold mb-4 text-center">阿里系参股企业</h3>
<div class="table-responsive">
<table class="table table-xs sm:table-md glass-effect">
<thead>
<tr>
<th>股票名称</th>
<th>分类</th>
<th>行业</th>
<th>相关性</th>
<th>持股比例</th>
</tr>
</thead>
<tbody>
<tr class="hover">
<td class="font-bold">石基信息</td>
<td><span class="badge badge-info">参股消费</span></td>
<td>信息技术</td>
<td>酒店直连核心</td>
<td><span class="badge badge-warning">13.02%</span></td>
</tr>
<tr class="hover">
<td class="font-bold">朗新集团</td>
<td><span class="badge badge-info">参股TMT</span></td>
<td>信息技术</td>
<td>能源数字化</td>
<td><span class="badge badge-warning">16.63%</span></td>
</tr>
<tr class="hover">
<td class="font-bold">千方科技</td>
<td><span class="badge badge-info">参股TMT</span></td>
<td>信息技术</td>
<td>交通领域唯一伙伴</td>
<td><span class="badge badge-warning">14.11%</span></td>
</tr>
<tr class="hover">
<td class="font-bold">分众传媒</td>
<td><span class="badge badge-info">参股TMT</span></td>
<td>传媒</td>
<td>营销合作</td>
<td><span class="badge badge-secondary">6.13%</span></td>
</tr>
<tr class="hover">
<td class="font-bold">三江购物</td>
<td><span class="badge badge-info">参股消费</span></td>
<td>零售</td>
<td>新零售布局</td>
<td><span class="badge badge-warning">30%</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Table 2 -->
<div class="mb-12">
<h3 class="text-2xl font-bold mb-4 text-center">业务合作企业</h3>
<div class="table-responsive">
<table class="table table-xs sm:table-md glass-effect">
<thead>
<tr>
<th>股票名称</th>
<th>项目/业务</th>
<th>原因</th>
<th>相关性</th>
</tr>
</thead>
<tbody>
<tr class="hover">
<td class="font-bold">恒银科技</td>
<td>灵光</td>
<td>接入千问大模型及灵光</td>
<td><span class="badge badge-success">直接合作</span></td>
</tr>
<tr class="hover">
<td class="font-bold">华策影视</td>
<td>内容制作</td>
<td>已接入千问大模型</td>
<td><span class="badge badge-info">应用合作</span></td>
</tr>
<tr class="hover">
<td class="font-bold">润建股份</td>
<td>智算云业务</td>
<td>与阿里云共同投资智算中心</td>
<td><span class="badge badge-success">基础设施</span></td>
</tr>
<tr class="hover">
<td class="font-bold">数据港</td>
<td>ZH13数据中心</td>
<td>阿里核心数据中心供应商</td>
<td><span class="badge badge-error">核心标的</span></td>
</tr>
<tr class="hover">
<td class="font-bold">杭钢股份</td>
<td>B栋数据中心</td>
<td>已上电机柜1069个</td>
<td><span class="badge badge-warning">算力需求</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Table 3 -->
<div class="mb-12">
<h3 class="text-2xl font-bold mb-4 text-center">技术供应商</h3>
<div class="table-responsive">
<table class="table table-xs sm:table-md glass-effect">
<thead>
<tr>
<th>股票名称</th>
<th>产品/服务</th>
<th>合作内容</th>
<th>类别</th>
</tr>
</thead>
<tbody>
<tr class="hover">
<td class="font-bold">浪潮信息</td>
<td>服务器</td>
<td>阿里服务器采购份额最高</td>
<td><span class="badge badge-primary">硬件</span></td>
</tr>
<tr class="hover">
<td class="font-bold">中际旭创</td>
<td>光模块</td>
<td>阿里云主要供应商</td>
<td><span class="badge badge-primary">硬件</span></td>
</tr>
<tr class="hover">
<td class="font-bold">紫光股份</td>
<td>交换机</td>
<td>400G交换机大份额</td>
<td><span class="badge badge-primary">硬件</span></td>
</tr>
<tr class="hover">
<td class="font-bold">寒武纪</td>
<td>GPU</td>
<td>云端产品线合作</td>
<td><span class="badge badge-warning">芯片</span></td>
</tr>
<tr class="hover">
<td class="font-bold">英维克</td>
<td>液冷温控</td>
<td>阿里数据中心制冷</td>
<td><span class="badge badge-info">配套</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Risk Analysis -->
<section class="py-16 bg-base-200">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
<i class="ri-error-warning-line text-red-500"></i> 风险提示
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="card glass-effect shadow-xl bg-red-900/20">
<div class="card-body text-center">
<i class="ri-code-box-line text-4xl text-red-400 mb-2"></i>
<h3 class="card-title justify-center">技术风险</h3>
<p class="text-xs">推理能力瓶颈</p>
<p class="text-xs">多模态整合瑕疵</p>
<p class="text-xs">自研芯片性能差距</p>
</div>
</div>
<div class="card glass-effect shadow-xl bg-orange-900/20">
<div class="card-body text-center">
<i class="ri-money-cny-circle-line text-4xl text-orange-400 mb-2"></i>
<h3 class="card-title justify-center">商业化风险</h3>
<p class="text-xs">变现模式不清晰</p>
<p class="text-xs">ROI压力巨大</p>
<p class="text-xs">用户留存不确定</p>
</div>
</div>
<div class="card glass-effect shadow-xl bg-yellow-900/20">
<div class="card-body text-center">
<i class="ri-shield-line text-4xl text-yellow-400 mb-2"></i>
<h3 class="card-title justify-center">政策风险</h3>
<p class="text-xs">数据安全监管</p>
<p class="text-xs">跨境数据限制</p>
<p class="text-xs">反垄断压力</p>
</div>
</div>
<div class="card glass-effect shadow-xl bg-purple-900/20">
<div class="card-body text-center">
<i class="ri-team-line text-4xl text-purple-400 mb-2"></i>
<h3 class="card-title justify-center">竞争风险</h3>
<p class="text-xs">字节豆包竞争</p>
<p class="text-xs">价格战内卷</p>
<p class="text-xs">同质化严重</p>
</div>
</div>
</div>
</div>
</section>
<!-- Investment Insights -->
<section class="py-16 bg-base-300">
<div class="container mx-auto px-4">
<h2 class="text-4xl font-bold text-center mb-12">
<i class="ri-lightbulb-line text-yellow-400"></i> 投资启示
</h2>
<div class="alert alert-info shadow-lg glass-effect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">投资策略建议</h3>
<ul class="list-disc list-inside space-y-2 mt-2 text-sm">
<li><strong>首选标的:</strong>石基信息002153.SZ- 阿里二股东+酒店场景独占50-70%上行空间</li>
<li><strong>次选标的:</strong>数据港603881.SH- 算力需求直接受益PS 4倍存重估空间</li>
<li><strong>防御配置:</strong>值得买300785.SZ- 稳定数据服务10-15%仓位</li>
<li><strong>关键跟踪指标:</strong>千问DAU、AI收入占比、Agent任务完成率</li>
<li><strong>仓位建议:</strong>总体控制在10-15%设置15%止损线</li>
</ul>
</div>
</div>
<div class="mt-8 text-center text-sm opacity-70">
<p>⚠️ 风险提示:本概念当前市梦率成分较重,请谨慎投资</p>
<p>核心观测期2025年4月Q1财报+Qwen4发布</p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer footer-center p-10 bg-base-300 text-base-content">
<div>
<p class="font-bold">阿里AI千问、灵光 - 概念深度分析</p>
<p>数据来源:公开信息、路演纪要、研究报告</p>
</div>
<div>
<div class="grid grid-flow-col gap-4">
<a><i class="ri-github-fill text-2xl"></i></a>
<a><i class="ri-twitter-fill text-2xl"></i></a>
<a><i class="ri-wechat-fill text-2xl"></i></a>
</div>
</div>
<div>
<p>© 2025 AI概念分析平台</p>
</div>
</footer>
<script>
// Add smooth scrolling
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
// Add animation on scroll
const observerOptions = {
threshold: 0.1
};
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, .alert, .timeline-dot').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.5s, transform 0.5s';
observer.observe(el);
});
</script>
</body>
</html>

View File

@@ -20,4 +20,5 @@ gevent-websocket==0.10.1
psutil==5.9.6
Pillow==10.1.0
itsdangerous==2.1.2
APScheduler==3.10.4
APScheduler==3.10.4
elasticsearch==8.15.0

View File

@@ -27,17 +27,66 @@ import { PerformancePanel } from './components/PerformancePanel';
import { useGlobalErrorHandler } from './hooks/useGlobalErrorHandler';
// Redux
import { initializePostHog } from './store/slices/posthogSlice';
// ⚡ PostHog 延迟加载:移除同步导入,首屏减少 ~180KB
// import { initializePostHog } from './store/slices/posthogSlice';
import { updateScreenSize } from './store/slices/deviceSlice';
import { injectReducer } from './store';
// Utils
import { logger } from './utils/logger';
import { performanceMonitor } from './utils/performanceMonitor';
// PostHog 追踪
import { trackEvent, trackEventAsync } from '@lib/posthog';
// PostHog 延迟加载:移除同步导入
// import { trackEvent, trackEventAsync } from '@lib/posthog';
// Contexts
import { useAuth } from '@contexts/AuthContext';
// ⚡ PostHog 延迟加载模块(动态导入后缓存)
let posthogModule = null;
let posthogSliceModule = null;
/**
* ⚡ 延迟加载 PostHog 模块
* 返回 { trackEvent, trackEventAsync, initializePostHog, posthogReducer }
*/
const loadPostHogModules = async () => {
if (posthogModule && posthogSliceModule) {
return { posthogModule, posthogSliceModule };
}
try {
const [posthog, posthogSlice] = await Promise.all([
import('@lib/posthog'),
import('./store/slices/posthogSlice'),
]);
posthogModule = posthog;
posthogSliceModule = posthogSlice;
return { posthogModule, posthogSliceModule };
} catch (error) {
logger.error('App', 'PostHog 模块加载失败', error);
return null;
}
};
/**
* ⚡ 异步追踪事件(延迟加载 PostHog 后调用)
* @param {string} eventName - 事件名称
* @param {object} properties - 事件属性
*/
const trackEventLazy = async (eventName, properties = {}) => {
// 等待模块加载完成
if (!posthogModule) {
const modules = await loadPostHogModules();
if (!modules) return;
}
// 使用异步追踪,不阻塞主线程
posthogModule.trackEventAsync(eventName, properties);
};
/**
* AppContent - 应用核心内容
* 负责 PostHog 初始化和渲染路由
@@ -51,28 +100,98 @@ function AppContent() {
const pageEnterTimeRef = useRef(Date.now());
const currentPathRef = useRef(location.pathname);
// 🎯 PostHog Redux 初始化
// 🎯 PostHog 空闲时加载 + Redux 初始化(首屏不加载 ~180KB
useEffect(() => {
dispatch(initializePostHog());
logger.info('App', 'PostHog Redux 初始化已触发');
const initPostHogRedux = async () => {
try {
const modules = await loadPostHogModules();
if (!modules) return;
const { posthogSliceModule } = modules;
// 动态注入 PostHog reducer
injectReducer('posthog', posthogSliceModule.default);
// 初始化 PostHog
dispatch(posthogSliceModule.initializePostHog());
// ⚡ 刷新注入前缓存的事件(避免丢失)
const pendingEvents = posthogSliceModule.flushPendingEventsBeforeInjection();
if (pendingEvents.length > 0) {
logger.info('App', `刷新 ${pendingEvents.length} 个注入前缓存的事件`);
pendingEvents.forEach(({ eventName, properties }) => {
posthogModule.trackEventAsync(eventName, properties);
});
}
logger.info('App', 'PostHog 模块空闲时加载完成Redux 初始化已触发');
} catch (error) {
logger.error('App', 'PostHog 加载失败', error);
}
};
// ⚡ 使用 requestIdleCallback 在浏览器空闲时加载,最长等待 3 秒
if ('requestIdleCallback' in window) {
const idleId = requestIdleCallback(initPostHogRedux, { timeout: 3000 });
return () => cancelIdleCallback(idleId);
} else {
// 降级Safari 等不支持 requestIdleCallback 的浏览器使用 setTimeout
const timer = setTimeout(initPostHogRedux, 1000);
return () => clearTimeout(timer);
}
}, [dispatch]);
// ✅ 首次访问追踪
// ⚡ 性能监控:标记 React 初始化完成
useEffect(() => {
performanceMonitor.mark('react-ready');
}, []);
// 📱 设备检测:监听窗口尺寸变化
useEffect(() => {
let resizeTimer;
const handleResize = () => {
// 防抖:避免频繁触发
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
dispatch(updateScreenSize());
}, 150);
};
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
return () => {
clearTimeout(resizeTimer);
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
};
}, [dispatch]);
// ✅ 首次访问追踪(🔴 关键事件:立即加载模块,确保数据不丢失)
useEffect(() => {
const hasVisited = localStorage.getItem('has_visited');
if (!hasVisited) {
const urlParams = new URLSearchParams(location.search);
// ⚡ 使用异步追踪,不阻塞页面渲染
trackEventAsync('first_visit', {
const eventData = {
referrer: document.referrer || 'direct',
utm_source: urlParams.get('utm_source'),
utm_medium: urlParams.get('utm_medium'),
utm_campaign: urlParams.get('utm_campaign'),
landing_page: location.pathname,
timestamp: new Date().toISOString()
});
};
// 🔴 关键事件:立即加载 PostHog 模块并同步追踪(不使用 trackEventLazy
// 确保首次访问数据不会因用户快速离开而丢失
(async () => {
const modules = await loadPostHogModules();
if (modules) {
// 使用同步追踪trackEvent而非异步追踪trackEventAsync
modules.posthogModule.trackEvent('first_visit', eventData);
logger.info('App', '首次访问事件已同步追踪', eventData);
}
})();
localStorage.setItem('has_visited', 'true');
}
@@ -87,8 +206,8 @@ function AppContent() {
// 只追踪停留时间 > 1 秒的页面(过滤快速跳转)
if (duration > 1) {
// ⚡ 使用异步追踪,不阻塞页面切换
trackEventAsync('page_view_duration', {
// ⚡ 使用延迟加载的异步追踪,不阻塞页面切换
trackEventLazy('page_view_duration', {
path: currentPathRef.current,
duration_seconds: duration,
is_authenticated: isAuthenticated,

View File

@@ -15,6 +15,10 @@
import { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
// ⚡ 模块级变量:防止 React StrictMode 双重初始化
let widgetInitialized = false;
let idleCallbackId = null;
const BytedeskWidget = ({
config,
autoLoad = true,
@@ -27,110 +31,151 @@ const BytedeskWidget = ({
useEffect(() => {
// 如果不自动加载或配置未设置,跳过
if (!autoLoad || !config) {
if (!config) {
console.warn('[Bytedesk] 配置未设置,客服组件未加载');
}
return;
}
console.log('[Bytedesk] 开始加载客服Widget...', config);
// ⚡ 防止重复初始化React StrictMode 会双重调用 useEffect
if (widgetInitialized) {
return;
}
// 加载Bytedesk Widget脚本
const script = document.createElement('script');
script.src = 'https://www.weiyuai.cn/embed/bytedesk-web.js';
script.async = true;
script.id = 'bytedesk-web-script';
// ⚡ 使用 requestIdleCallback 延迟加载,不阻塞首屏
const loadWidget = () => {
// 再次检查,防止竞态条件
if (widgetInitialized) return;
widgetInitialized = true;
script.onload = () => {
console.log('[Bytedesk] Widget脚本加载成功');
// 检查脚本是否已存在
if (document.getElementById('bytedesk-web-script')) {
return;
}
try {
if (window.BytedeskWeb) {
console.log('[Bytedesk] 初始化Widget');
const bytedesk = new window.BytedeskWeb(config);
bytedesk.init();
// 加载Bytedesk Widget脚本
const script = document.createElement('script');
script.src = 'https://www.weiyuai.cn/embed/bytedesk-web.js';
script.async = true;
script.id = 'bytedesk-web-script';
widgetRef.current = bytedesk;
console.log('[Bytedesk] Widget初始化成功');
script.onload = () => {
try {
if (window.BytedeskWeb) {
const bytedesk = new window.BytedeskWeb(config);
bytedesk.init();
widgetRef.current = bytedesk;
// ⚡ 屏蔽 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; // 不输出日志
// ⚡ H5 端样式适配:使用 MutationObserver 立即应用样式(避免闪烁
const isMobile = window.innerWidth <= 768;
const applyBytedeskStyles = () => {
const allElements = document.querySelectorAll('body > div');
allElements.forEach(el => {
const style = window.getComputedStyle(el);
// 检查是否是右下角固定定位的元素Bytedesk 按钮)
if (style.position === 'fixed' && style.right && style.bottom) {
const rightVal = parseInt(style.right);
const bottomVal = parseInt(style.bottom);
if (rightVal >= 0 && rightVal < 100 && bottomVal >= 0 && bottomVal < 100) {
// H5 端设置按钮尺寸为 48x48只执行一次
if (isMobile && !el.dataset.bytedeskStyled) {
el.dataset.bytedeskStyled = 'true';
const button = el.querySelector('button');
if (button) {
button.style.width = '48px';
button.style.height = '48px';
button.style.minWidth = '48px';
button.style.minHeight = '48px';
}
}
// 提示框 3 秒后隐藏(查找白色气泡框)
const children = el.querySelectorAll('div');
children.forEach(child => {
if (child.dataset.bytedeskTooltip) return; // 已处理过
const childStyle = window.getComputedStyle(child);
// 白色背景的提示框
if (childStyle.backgroundColor === 'rgb(255, 255, 255)') {
child.dataset.bytedeskTooltip = 'true';
setTimeout(() => {
child.style.transition = 'opacity 0.3s';
child.style.opacity = '0';
setTimeout(() => child.remove(), 300);
}, 3000);
}
});
}
}
});
};
// 立即执行一次
applyBytedeskStyles();
// 监听 DOM 变化,新元素出现时立即应用样式
const observer = new MutationObserver(applyBytedeskStyles);
observer.observe(document.body, { childList: true, subtree: true });
// 5 秒后停止监听(避免性能问题)
setTimeout(() => observer.disconnect(), 5000);
// ⚡ 屏蔽 STOMP WebSocket 错误日志(不影响功能)
const originalConsoleError = console.error;
console.error = function(...args) {
const errorMsg = args.join(' ');
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);
}
originalConsoleError.apply(console, args);
};
if (onLoad) {
onLoad(bytedesk);
} else {
throw new Error('BytedeskWeb对象未定义');
}
} catch (error) {
console.error('[Bytedesk] 初始化失败:', error);
if (onError) {
onError(error);
}
} else {
throw new Error('BytedeskWeb对象未定义');
}
} catch (error) {
console.error('[Bytedesk] Widget初始化失败:', error);
};
script.onerror = (error) => {
console.error('[Bytedesk] 脚本加载失败:', error);
widgetInitialized = false; // 允许重试
if (onError) {
onError(error);
}
}
};
document.body.appendChild(script);
scriptRef.current = script;
};
script.onerror = (error) => {
console.error('[Bytedesk] Widget脚本加载失败:', error);
if (onError) {
onError(error);
}
};
// ⚡ 使用 requestIdleCallback 在浏览器空闲时加载
if ('requestIdleCallback' in window) {
idleCallbackId = requestIdleCallback(loadWidget, { timeout: 3000 });
} else {
// 降级:使用 setTimeout
idleCallbackId = setTimeout(loadWidget, 100);
}
// 添加脚本到页面
document.body.appendChild(script);
scriptRef.current = script;
// 清理函数 - 增强错误处理,防止 React 18 StrictMode 双重清理报错
// 清理函数
return () => {
console.log('[Bytedesk] 清理Widget');
// 移除脚本
try {
if (scriptRef.current && scriptRef.current.parentNode) {
scriptRef.current.parentNode.removeChild(scriptRef.current);
// 取消待执行的 idle callback
if (idleCallbackId) {
if ('cancelIdleCallback' in window) {
cancelIdleCallback(idleCallbackId);
} else {
clearTimeout(idleCallbackId);
}
scriptRef.current = null;
} catch (error) {
console.warn('[Bytedesk] 移除脚本失败(可能已被移除):', error.message);
idleCallbackId = null;
}
// 移除Widget DOM元素
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);
}
// 清理全局对象
try {
if (window.BytedeskWeb) {
delete window.BytedeskWeb;
}
} catch (error) {
console.warn('[Bytedesk] 清理全局对象失败:', error.message);
}
// ⚠️ 不重置 widgetInitialized保持单例
// 不清理 DOM因为客服 Widget 应该持久存在
};
}, [config, autoLoad, onLoad, onError]);

View File

@@ -216,12 +216,6 @@ export default function AuthFormContent() {
authEvents.trackVerificationCodeSent(credential, config.api.purpose);
}
// ❌ 移除成功 toast静默处理
logger.info('AuthFormContent', '验证码发送成功', {
credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7),
dev_code: data.dev_code
});
// ✅ 开发环境下在控制台显示验证码
if (data.dev_code) {
console.log(`%c✅ [验证码] ${cleanedCredential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
@@ -328,16 +322,6 @@ export default function AuthFormContent() {
}
if (response.ok && data.success) {
// ⚡ Mock 模式:先在前端侧写入 localStorage确保时序正确
if (process.env.REACT_APP_ENABLE_MOCK === 'true' && data.user) {
setCurrentUser(data.user);
logger.debug('AuthFormContent', '前端侧设置当前用户(Mock模式)', {
userId: data.user?.id,
phone: data.user?.phone,
mockMode: true
});
}
// 更新session
await checkSession();
@@ -476,7 +460,8 @@ export default function AuthFormContent() {
return () => {
isMountedRef.current = false;
};
}, [authEvents]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 仅在挂载时执行一次,避免 countdown 倒计时导致重复触发
return (
<>

View File

@@ -74,6 +74,7 @@ export default function WechatRegister() {
const isMountedRef = useRef(true); // 追踪组件挂载状态
const containerRef = useRef(null); // 容器DOM引用
const sessionIdRef = useRef(null); // 存储最新的 sessionId避免闭包陷阱
const wechatStatusRef = useRef(WECHAT_STATUS.NONE); // 存储最新的 wechatStatus避免闭包陷阱
const navigate = useNavigate();
const toast = useToast();
@@ -128,12 +129,8 @@ export default function WechatRegister() {
*/
const handleLoginSuccess = useCallback(async (sessionId, status) => {
try {
logger.info('WechatRegister', '开始调用登录接口', { sessionId: sessionId.substring(0, 8) + '...', status });
const response = await authService.loginWithWechat(sessionId);
logger.info('WechatRegister', '登录接口返回', { success: response?.success, hasUser: !!response?.user });
if (response?.success) {
// 追踪微信登录成功
authEvents.trackLoginSuccess(
@@ -182,40 +179,28 @@ export default function WechatRegister() {
const checkWechatStatus = useCallback(async () => {
// 检查组件是否已卸载,使用 ref 获取最新的 sessionId
if (!isMountedRef.current || !sessionIdRef.current) {
logger.debug('WechatRegister', 'checkWechatStatus 跳过', {
isMounted: isMountedRef.current,
hasSessionId: !!sessionIdRef.current
});
return;
}
const currentSessionId = sessionIdRef.current;
logger.debug('WechatRegister', '检查微信状态', { sessionId: currentSessionId });
try {
const response = await authService.checkWechatStatus(currentSessionId);
// 安全检查:确保 response 存在且包含 status
if (!response || typeof response.status === 'undefined') {
logger.warn('WechatRegister', '微信状态检查返回无效数据', { response });
return;
}
const { status } = response;
logger.debug('WechatRegister', '微信状态', { status });
logger.debug('WechatRegister', '检测到微信状态', {
sessionId: wechatSessionId.substring(0, 8) + '...',
status,
userInfo: response.user_info
});
// 组件卸载后不再更新状态
if (!isMountedRef.current) return;
// 追踪状态变化
if (wechatStatus !== status) {
authEvents.trackWechatStatusChanged(currentSessionId, wechatStatus, status);
// 追踪状态变化(使用 ref 获取最新状态,避免闭包陷阱)
const previousStatus = wechatStatusRef.current;
if (previousStatus !== status) {
authEvents.trackWechatStatusChanged(currentSessionId, previousStatus, status);
// 特别追踪扫码事件
if (status === WECHAT_STATUS.SCANNED) {
@@ -227,7 +212,6 @@ export default function WechatRegister() {
// 处理成功状态
if (status === WECHAT_STATUS.LOGIN_SUCCESS || status === WECHAT_STATUS.REGISTER_SUCCESS) {
logger.info('WechatRegister', '检测到登录成功状态,停止轮询', { status });
clearTimers(); // 停止轮询
sessionIdRef.current = null; // 清理 sessionId
@@ -277,6 +261,12 @@ export default function WechatRegister() {
});
}
}
// 处理授权成功AUTHORIZED- 用户已在微信端确认授权,调用登录 API
else if (status === WECHAT_STATUS.AUTHORIZED) {
clearTimers();
sessionIdRef.current = null; // 清理 sessionId
await handleLoginSuccess(currentSessionId, status);
}
} catch (error) {
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId });
// 轮询过程中的错误不显示给用户,避免频繁提示
@@ -301,11 +291,6 @@ export default function WechatRegister() {
* 启动轮询
*/
const startPolling = useCallback(() => {
logger.debug('WechatRegister', '启动轮询', {
sessionId: sessionIdRef.current,
interval: POLL_INTERVAL
});
// 清理旧的定时器
clearTimers();
@@ -316,7 +301,6 @@ export default function WechatRegister() {
// 设置超时
timeoutRef.current = setTimeout(() => {
logger.debug('WechatRegister', '二维码超时');
clearTimers();
sessionIdRef.current = null; // 清理 sessionId
setWechatStatus(WECHAT_STATUS.EXPIRED);
@@ -368,11 +352,6 @@ export default function WechatRegister() {
setWechatSessionId(response.data.session_id);
setWechatStatus(WECHAT_STATUS.WAITING);
logger.debug('WechatRegister', '获取二维码成功', {
sessionId: response.data.session_id,
authUrl: response.data.auth_url
});
// 启动轮询检查扫码状态
startPolling();
} catch (error) {
@@ -404,6 +383,14 @@ export default function WechatRegister() {
}
}, [getWechatQRCode]);
/**
* 同步 wechatStatusRef 与 wechatStatus state
* 确保 checkWechatStatus 回调中能获取到最新状态
*/
useEffect(() => {
wechatStatusRef.current = wechatStatus;
}, [wechatStatus]);
/**
* 组件卸载时清理定时器和标记组件状态
*/

View File

@@ -1,40 +1,52 @@
import { Link } from "react-router-dom";
import { svgs } from "./svgs";
import React from 'react';
import { Link } from 'react-router-dom';
import { svgs } from './svgs';
const Button = ({
className,
href,
onClick,
children,
px,
white,
interface ButtonProps {
className?: string;
href?: string;
onClick?: () => void;
children?: React.ReactNode;
px?: string;
white?: boolean;
isPrimary?: boolean;
isSecondary?: boolean;
}
const Button: React.FC<ButtonProps> = ({
className,
href,
onClick,
children,
px,
white,
}) => {
const classes = `button relative inline-flex items-center justify-center h-11 ${
px || "px-7"
} ${white ? "text-n-8" : "text-n-1"} transition-colors hover:text-color-1 ${
className || ""
}`;
const classes = `button relative inline-flex items-center justify-center h-11 ${
px || 'px-7'
} ${white ? 'text-n-8' : 'text-n-1'} transition-colors hover:text-color-1 ${
className || ''
}`;
const spanClasses = `relative z-10`;
const spanClasses = `relative z-10`;
return href ? (
href.startsWith("mailto:") ? (
<a href={href} className={classes}>
<span className={spanClasses}>{children}</span>
{svgs(white)}
</a>
) : (
<Link href={href} className={classes}>
<span className={spanClasses}>{children}</span>
{svgs(white)}
</Link>
)
return href ? (
href.startsWith('mailto:') ? (
<a href={href} className={classes}>
<span className={spanClasses}>{children}</span>
{svgs(white)}
</a>
) : (
<button className={classes} onClick={onClick}>
<span className={spanClasses}>{children}</span>
{svgs(white)}
</button>
);
<Link to={href} className={classes}>
<span className={spanClasses}>{children}</span>
{svgs(white)}
</Link>
)
) : (
<button className={classes} onClick={onClick}>
<span className={spanClasses}>{children}</span>
{svgs(white)}
</button>
);
};
export default Button;

View File

@@ -1,53 +0,0 @@
import React from "react";
import Link, { LinkProps } from "next/link";
type CommonProps = {
className?: string;
children?: React.ReactNode;
isPrimary?: boolean;
isSecondary?: boolean;
};
type ButtonAsButton = {
as?: "button";
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
type ButtonAsAnchor = {
as: "a";
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
type ButtonAsLink = {
as: "link";
} & LinkProps;
type ButtonProps = CommonProps &
(ButtonAsButton | ButtonAsAnchor | ButtonAsLink);
const Button: React.FC<ButtonProps> = ({
className,
children,
isPrimary,
isSecondary,
as = "button",
...props
}) => {
const isLink = as === "link";
const Component: React.ElementType = isLink ? Link : as;
return (
<Component
className={`relative inline-flex justify-center items-center h-10 px-3.5 rounded-lg text-title-5 cursor-pointer transition-all ${
isPrimary ? "bg-white text-black hover:bg-white/90" : ""
} ${
isSecondary
? "shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.10)_inset] text-white after:absolute after:inset-0 after:border after:border-line after:rounded-lg after:pointer-events-none after:transition-colors hover:after:border-white"
: ""
} ${className || ""}`}
{...(isLink ? (props as LinkProps) : props)}
>
{children}
</Component>
);
};
export default Button;

View File

@@ -82,29 +82,9 @@ const CitedContent = ({
...containerStyle
}}
>
{/* AI 标识 - 固定在右上角 */}
{showAIBadge && (
<Tag
icon={<RobotOutlined />}
color="purple"
style={{
position: 'absolute',
top: 12,
right: 12,
margin: 0,
zIndex: 10,
fontSize: 12,
padding: '2px 8px'
}}
className="ai-badge-responsive"
>
AI合成
</Tag>
)}
{/* 标题栏 */}
{title && (
<div style={{ marginBottom: 12, paddingRight: 80 }}>
<div style={{ marginBottom: 12 }}>
<Text strong style={{ fontSize: 14, color: finalTitleColor }}>
{title}
</Text>
@@ -112,10 +92,24 @@ const CitedContent = ({
)}
{/* 带引用的文本内容 */}
<div style={{
lineHeight: 1.8,
paddingRight: title ? 0 : (showAIBadge ? 80 : 0)
}}>
<div style={{ lineHeight: 1.8 }}>
{/* AI 标识 - 行内显示在文字前面 */}
{showAIBadge && (
<Tag
icon={<RobotOutlined />}
color="purple"
style={{
fontSize: 12,
padding: '2px 8px',
marginRight: 8,
verticalAlign: 'middle',
display: 'inline-flex',
}}
className="ai-badge-responsive"
>
AI合成
</Tag>
)}
{/* 前缀标签(如果有) */}
{prefix && (
<Text style={{

View File

@@ -1,20 +1,25 @@
import Image from "../Image";
import React from 'react';
import Image from '../Image';
const Generating = ({ className }) => (
<div
className={`flex items-center h-[3.375rem] px-6 bg-n-8/80 rounded-[1.6875rem] ${
className || ""
} text-base`}
>
<Image
className="w-5 h-5 mr-4"
src="/images/loading.png"
width={20}
height={20}
alt="Loading"
/>
AI is generating|
</div>
interface GeneratingProps {
className?: string;
}
const Generating: React.FC<GeneratingProps> = ({ className }) => (
<div
className={`flex items-center h-[3.375rem] px-6 bg-n-8/80 rounded-[1.6875rem] ${
className || ''
} text-base`}
>
<Image
className="w-5 h-5 mr-4"
src="/images/loading.png"
width={20}
height={20}
alt="Loading"
/>
AI is generating|
</div>
);
export default Generating;

View File

@@ -2,95 +2,48 @@
// 集中管理应用的全局组件
import React, { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useNotification } from '../contexts/NotificationContext';
import { logger } from '../utils/logger';
import { useSelector } from 'react-redux';
import { selectIsMobile } from '@/store/slices/deviceSlice';
// Global Components
import AuthModalManager from './Auth/AuthModalManager';
import NotificationContainer from './NotificationContainer';
import ConnectionStatusBar from './ConnectionStatusBar';
import ScrollToTop from './ScrollToTop';
// Bytedesk客服组件
import BytedeskWidget from '../bytedesk-integration/components/BytedeskWidget';
import { getBytedeskConfig } from '../bytedesk-integration/config/bytedesk.config';
/**
* ConnectionStatusBar 包装组件
* 需要在 NotificationProvider 内部使用,所以在这里包装
*/
function ConnectionStatusBarWrapper() {
const { connectionStatus, reconnectAttempt, maxReconnectAttempts, retryConnection } = useNotification();
const [isDismissed, setIsDismissed] = React.useState(false);
// 监听连接状态变化
React.useEffect(() => {
// 重连成功后,清除 dismissed 状态
if (connectionStatus === 'connected' && isDismissed) {
setIsDismissed(false);
// 从 localStorage 清除 dismissed 标记
localStorage.removeItem('connection_status_dismissed');
}
// 从 localStorage 恢复 dismissed 状态
if (connectionStatus !== 'connected' && !isDismissed) {
const dismissed = localStorage.getItem('connection_status_dismissed');
if (dismissed === 'true') {
setIsDismissed(true);
}
}
}, [connectionStatus, isDismissed]);
const handleClose = () => {
// 用户手动关闭,保存到 localStorage
setIsDismissed(true);
localStorage.setItem('connection_status_dismissed', 'true');
logger.info('App', 'Connection status bar dismissed by user');
};
return (
<ConnectionStatusBar
status={connectionStatus}
reconnectAttempt={reconnectAttempt}
maxReconnectAttempts={maxReconnectAttempts}
onRetry={retryConnection}
onClose={handleClose}
isDismissed={isDismissed}
/>
);
}
/**
* GlobalComponents - 全局组件容器
* 集中管理所有全局级别的组件,如弹窗、通知、状态栏等
*
* 包含的组件:
* - ConnectionStatusBarWrapper: Socket 连接状态条
* - ScrollToTop: 路由切换时自动滚动到顶部
* - AuthModalManager: 认证弹窗管理器
* - NotificationContainer: 通知容器
* - BytedeskWidget: Bytedesk在线客服 (条件性显示,在/和/home页隐藏)
* - NotificationContainer: 通知容器(仅桌面端渲染)
* - BytedeskWidget: Bytedesk在线客服
*
* 注意:
* - ConnectionStatusBar 已移除(所有端)
* - NotificationContainer 在移动端不渲染(通知功能已在 NotificationContext 层禁用)
*/
export function GlobalComponents() {
const location = useLocation();
const isMobile = useSelector(selectIsMobile);
// ✅ 缓存 Bytedesk 配置对象,避免每次渲染都创建新引用导致重新加载
const bytedeskConfigMemo = useMemo(() => getBytedeskConfig(), []);
return (
<>
{/* Socket 连接状态条 */}
<ConnectionStatusBarWrapper />
{/* 路由切换时自动滚动到顶部 */}
<ScrollToTop />
{/* 认证弹窗管理器 */}
<AuthModalManager />
{/* 通知容器 */}
<NotificationContainer />
{/* 通知容器(仅桌面端渲染) */}
{!isMobile && <NotificationContainer />}
{/* Bytedesk在线客服 - 使用缓存的配置对象 */}
<BytedeskWidget

View File

@@ -1,17 +1,21 @@
import { useState } from "react";
import React, { useState } from 'react';
const Image = ({ className, ...props }) => {
const [loaded, setLoaded] = useState(false);
interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
className?: string;
}
return (
<img
className={`inline-block align-top opacity-0 transition-opacity ${
loaded && "opacity-100"
} ${className}`}
onLoad={() => setLoaded(true)}
{...props}
/>
);
const Image: React.FC<ImageProps> = ({ className, ...props }) => {
const [loaded, setLoaded] = useState(false);
return (
<img
className={`inline-block align-top opacity-0 transition-opacity ${
loaded && 'opacity-100'
} ${className || ''}`}
onLoad={() => setLoaded(true)}
{...props}
/>
);
};
export default Image;

View File

@@ -1,73 +0,0 @@
import Section from "@/components/Section";
import Image from "@/components/Image";
import Button from "@/components/Button";
type JoinProps = {};
const Join = ({}: JoinProps) => (
<Section crosses>
<div className="container">
<div className="relative max-w-[43.125rem] mx-auto py-8 md:py-14 xl:py-0">
<div className="relative z-1 text-center">
<h1 className="h1 mb-6">
Be part of the future of{" "}
<span className="inline-block relative">
Brainwave
<Image
className="absolute top-full left-0 w-full"
src="/images/curve.png"
width={624}
height={28}
alt="Curve"
/>
</span>
</h1>
<p className="body-1 mb-8 text-n-4">
Unleash the power of AI within Brainwave. Upgrade your
productivity with Brainwave, the open AI chat app.
</p>
<Button href="/pricing" white>
Get started
</Button>
</div>
<div className="absolute top-1/2 left-1/2 w-[46.5rem] h-[46.5rem] border border-n-2/5 rounded-full -translate-x-1/2 -translate-y-1/2 pointer-events-none">
<div className="absolute top-1/2 left-1/2 w-[39.25rem] h-[39.25rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
<div className="absolute top-1/2 left-1/2 w-[30.625rem] h-[30.625rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
<div className="absolute top-1/2 left-1/2 w-[21.5rem] h-[21.5rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
<div className="absolute top-1/2 left-1/2 w-[13.75rem] h-[13.75rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
</div>
<div className="absolute top-1/2 left-1/2 w-[46.5rem] h-[46.5rem] border border-n-2/5 rounded-full -translate-x-1/2 -translate-y-1/2 opacity-60 mix-blend-color-dodge pointer-events-none">
<div className="absolute top-1/2 left-1/2 w-[58.85rem] h-[58.85rem] -translate-x-3/4 -translate-y-1/2">
<Image
className="w-full"
src="/images/gradient.png"
width={942}
height={942}
alt="Gradient"
/>
</div>
</div>
</div>
</div>
<div className="absolute -top-[5.75rem] left-[18.5rem] -z-1 w-[19.8125rem] pointer-events-none lg:-top-15 lg:left-[5.5rem]">
<Image
className="w-full"
src="/images/join/shapes-1.svg"
width={317}
height={293}
alt="Shapes 1"
/>
</div>
<div className="absolute right-[15rem] -bottom-[7rem] -z-1 w-[28.1875rem] pointer-events-none lg:right-7 lg:-bottom-[5rem]">
<Image
className="w-full"
src="/images/join/shapes-2.svg"
width={451}
height={266}
alt="Shapes 2"
/>
</div>
</Section>
);
export default Join;

View File

@@ -1,73 +0,0 @@
import Section from "@/components/Section";
import Image from "@/components/Image";
import Button from "@/components/Button";
type JoinProps = {};
const Join = ({}: JoinProps) => (
<Section crosses>
<div className="container">
<div className="relative max-w-[43.125rem] mx-auto py-8 md:py-14 xl:py-0">
<div className="relative z-1 text-center">
<h1 className="h1 mb-6">
Be part of the future of{" "}
<span className="inline-block relative">
Brainwave
<Image
className="absolute top-full left-0 w-full"
src="/images/curve.png"
width={624}
height={28}
alt="Curve"
/>
</span>
</h1>
<p className="body-1 mb-8 text-n-4">
Unleash the power of AI within Brainwave. Upgrade your
productivity with Brainwave, the open AI chat app.
</p>
<Button href="/pricing" white>
Get started
</Button>
</div>
<div className="absolute top-1/2 left-1/2 w-[46.5rem] h-[46.5rem] border border-n-2/5 rounded-full -translate-x-1/2 -translate-y-1/2 pointer-events-none">
<div className="absolute top-1/2 left-1/2 w-[39.25rem] h-[39.25rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
<div className="absolute top-1/2 left-1/2 w-[30.625rem] h-[30.625rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
<div className="absolute top-1/2 left-1/2 w-[21.5rem] h-[21.5rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
<div className="absolute top-1/2 left-1/2 w-[13.75rem] h-[13.75rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
</div>
<div className="absolute top-1/2 left-1/2 w-[46.5rem] h-[46.5rem] border border-n-2/5 rounded-full -translate-x-1/2 -translate-y-1/2 opacity-60 mix-blend-color-dodge pointer-events-none">
<div className="absolute top-1/2 left-1/2 w-[58.85rem] h-[58.85rem] -translate-x-3/4 -translate-y-1/2">
<Image
className="w-full"
src="/images/gradient.png"
width={942}
height={942}
alt="Gradient"
/>
</div>
</div>
</div>
</div>
<div className="absolute -top-[5.75rem] left-[18.5rem] -z-1 w-[19.8125rem] pointer-events-none lg:-top-15 lg:left-[5.5rem]">
<Image
className="w-full"
src="/images/join/shapes-1.svg"
width={317}
height={293}
alt="Shapes 1"
/>
</div>
<div className="absolute right-[15rem] -bottom-[7rem] -z-1 w-[28.1875rem] pointer-events-none lg:right-7 lg:-bottom-[5rem]">
<Image
className="w-full"
src="/images/join/shapes-2.svg"
width={451}
height={266}
alt="Shapes 2"
/>
</div>
</Section>
);
export default Join;

View File

@@ -1,53 +0,0 @@
import Image from "../Image";
const Logos = ({ className }) => (
<div className={className}>
<h5 className="tagline mb-6 text-center text-n-1/50">
Helping people create beautiful content at
</h5>
<ul className="flex">
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
</ul>
</div>
);
export default Logos;

View File

@@ -1,53 +0,0 @@
import Image from "../Image";
const Logos = ({ className }) => (
<div className={className}>
<h5 className="tagline mb-6 text-center text-n-1/50">
Helping people create beautiful content at
</h5>
<ul className="flex">
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
<Image
src="/images/yourlogo.svg"
width={134}
height={28}
alt="Logo 3"
/>
</li>
</ul>
</div>
);
export default Logos;

View File

@@ -18,10 +18,8 @@ import {
Link,
Divider,
Avatar,
useColorMode,
useColorModeValue
} from '@chakra-ui/react';
import { SunIcon, MoonIcon } from '@chakra-ui/icons';
import { useNavigate, useLocation } from 'react-router-dom';
/**
@@ -46,7 +44,6 @@ const MobileDrawer = memo(({
}) => {
const navigate = useNavigate();
const location = useLocation();
const { colorMode, toggleColorMode } = useColorMode();
const userBgColor = useColorModeValue('gray.50', 'whiteAlpha.100');
const contactTextColor = useColorModeValue('gray.500', 'gray.300');
const emailTextColor = useColorModeValue('gray.500', 'gray.300');
@@ -82,17 +79,6 @@ const MobileDrawer = memo(({
</DrawerHeader>
<DrawerBody>
<VStack spacing={4} align="stretch">
{/* 移动端:日夜模式切换 */}
<Button
leftIcon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
variant="ghost"
justifyContent="flex-start"
onClick={toggleColorMode}
size="sm"
>
切换到{colorMode === 'light' ? '深色' : '浅色'}模式
</Button>
{/* 移动端用户信息 */}
{isAuthenticated && user && (
<>

View File

@@ -2,7 +2,7 @@
// Navbar 右侧功能区组件
import React, { memo } from 'react';
import { HStack, Spinner, IconButton, Box } from '@chakra-ui/react';
import { HStack, IconButton, Box } from '@chakra-ui/react';
import { HamburgerIcon } from '@chakra-ui/icons';
// import ThemeToggleButton from '../ThemeToggleButton'; // ❌ 已删除 - 不再支持深色模式切换
import LoginButton from '../LoginButton';
@@ -41,9 +41,15 @@ const NavbarActions = memo(({
}) => {
return (
<HStack spacing={{ base: 2, md: 4 }}>
{/* 显示加载状态 */}
{/* 权限校验中 - 显示占位骨架,不显示登录按钮或用户菜单 */}
{isLoading ? (
<Spinner size="sm" color="blue.500" />
<Box
w={{ base: '80px', md: '120px' }}
h="36px"
borderRadius="md"
bg="whiteAlpha.100"
opacity={0.6}
/>
) : isAuthenticated && user ? (
// 已登录状态 - 用户菜单 + 功能菜单排列
<HStack spacing={{ base: 2, md: 3 }}>

View File

@@ -57,13 +57,14 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
<MenuButton
as={Button}
variant="ghost"
rightIcon={<ChevronDownIcon />}
bg={isActive(['/community', '/concepts']) ? 'blue.50' : 'transparent'}
color={isActive(['/community', '/concepts']) ? 'blue.600' : 'inherit'}
rightIcon={<ChevronDownIcon color={isActive(['/community', '/concepts']) ? 'white' : 'inherit'} />}
bg={isActive(['/community', '/concepts']) ? 'blue.600' : 'transparent'}
color={isActive(['/community', '/concepts']) ? 'white' : 'inherit'}
fontWeight={isActive(['/community', '/concepts']) ? 'bold' : 'normal'}
borderBottom={isActive(['/community', '/concepts']) ? '2px solid' : 'none'}
borderColor="blue.600"
_hover={{ bg: isActive(['/community', '/concepts']) ? 'blue.100' : 'gray.50' }}
borderLeft={isActive(['/community', '/concepts']) ? '3px solid' : 'none'}
borderColor="white"
borderRadius="md"
_hover={{ bg: isActive(['/community', '/concepts']) ? 'blue.700' : 'gray.50' }}
onMouseEnter={highFreqMenu.handleMouseEnter}
onMouseLeave={highFreqMenu.handleMouseLeave}
onClick={highFreqMenu.handleClick}
@@ -123,13 +124,14 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
<MenuButton
as={Button}
variant="ghost"
rightIcon={<ChevronDownIcon />}
bg={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.50' : 'transparent'}
color={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.600' : 'inherit'}
rightIcon={<ChevronDownIcon color={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'white' : 'inherit'} />}
bg={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.600' : 'transparent'}
color={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'white' : 'inherit'}
fontWeight={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'bold' : 'normal'}
borderBottom={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? '2px solid' : 'none'}
borderColor="blue.600"
_hover={{ bg: isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.100' : 'gray.50' }}
borderLeft={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? '3px solid' : 'none'}
borderColor="white"
borderRadius="md"
_hover={{ bg: isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.700' : 'gray.50' }}
onMouseEnter={marketReviewMenu.handleMouseEnter}
onMouseLeave={marketReviewMenu.handleMouseLeave}
onClick={marketReviewMenu.handleClick}
@@ -198,13 +200,14 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
<MenuButton
as={Button}
variant="ghost"
rightIcon={<ChevronDownIcon />}
bg={isActive(['/agent-chat']) ? 'blue.50' : 'transparent'}
color={isActive(['/agent-chat']) ? 'blue.600' : 'inherit'}
fontWeight={isActive(['/agent-chat']) ? 'bold' : 'normal'}
borderBottom={isActive(['/agent-chat']) ? '2px solid' : 'none'}
borderColor="blue.600"
_hover={{ bg: isActive(['/agent-chat']) ? 'blue.100' : 'gray.50' }}
rightIcon={<ChevronDownIcon color={isActive(['/agent-chat', '/value-forum']) ? 'white' : 'inherit'} />}
bg={isActive(['/agent-chat', '/value-forum']) ? 'blue.600' : 'transparent'}
color={isActive(['/agent-chat', '/value-forum']) ? 'white' : 'inherit'}
fontWeight={isActive(['/agent-chat', '/value-forum']) ? 'bold' : 'normal'}
borderLeft={isActive(['/agent-chat', '/value-forum']) ? '3px solid' : 'none'}
borderColor="white"
borderRadius="md"
_hover={{ bg: isActive(['/agent-chat', '/value-forum']) ? 'blue.700' : 'gray.50' }}
onMouseEnter={agentCommunityMenu.handleMouseEnter}
onMouseLeave={agentCommunityMenu.handleMouseLeave}
onClick={agentCommunityMenu.handleClick}

View File

@@ -1,49 +0,0 @@
import Image from "../Image";
const Notification = ({ className, title }) => (
<div
className={`flex items-center p-4 pr-6 bg-[#474060]/40 backdrop-blur border border-n-1/10 rounded-2xl ${
className || ""
}`}
>
<div className="mr-5">
<Image
className="w-full rounded-xl"
src="/images/notification/image-1.png"
width={52}
height={52}
alt="Image"
/>
</div>
<div className="flex-1">
<h6 className="mb-1 font-semibold text-base">{title}</h6>
<div className="flex items-center justify-between">
<ul className="flex -m-0.5">
{[
"/images/notification/image-4.png",
"/images/notification/image-3.png",
"/images/notification/image-2.png",
].map((item, index) => (
<li
className={`flex w-6 h-6 border-2 border-[#2E2A41] rounded-full overflow-hidden ${
index !== 0 ? "-ml-2" : ""
}`}
key={index}
>
<Image
className="w-full"
src={item}
width={20}
height={20}
alt={item}
/>
</li>
))}
</ul>
<div className="body-2 text-[#6C7275]">1m ago</div>
</div>
</div>
</div>
);
export default Notification;

View File

@@ -1,49 +0,0 @@
import Image from "../Image";
const Notification = ({ className, title }) => (
<div
className={`flex items-center p-4 pr-6 bg-[#474060]/40 backdrop-blur border border-n-1/10 rounded-2xl ${
className || ""
}`}
>
<div className="mr-5">
<Image
className="w-full rounded-xl"
src="/images/notification/image-1.png"
width={52}
height={52}
alt="Image"
/>
</div>
<div className="flex-1">
<h6 className="mb-1 font-semibold text-base">{title}</h6>
<div className="flex items-center justify-between">
<ul className="flex -m-0.5">
{[
"/images/notification/image-4.png",
"/images/notification/image-3.png",
"/images/notification/image-2.png",
].map((item, index) => (
<li
className={`flex w-6 h-6 border-2 border-[#2E2A41] rounded-full overflow-hidden ${
index !== 0 ? "-ml-2" : ""
}`}
key={index}
>
<Image
className="w-full"
src={item}
width={20}
height={20}
alt={item}
/>
</li>
))}
</ul>
<div className="body-2 text-[#6C7275]">1m ago</div>
</div>
</div>
</div>
);
export default Notification;

View File

@@ -1,57 +1,67 @@
const Section = ({
className,
crosses,
crossesOffset,
customPaddings,
children,
import React from 'react';
interface SectionProps {
className?: string;
crosses?: boolean;
crossesOffset?: string;
customPaddings?: string;
children?: React.ReactNode;
}
const Section: React.FC<SectionProps> = ({
className,
crosses,
crossesOffset,
customPaddings,
children,
}) => (
<div
className={`relative ${
customPaddings ||
`py-10 lg:py-16 xl:py-20 ${crosses ? "lg:py-32 xl:py-40" : ""}`
} ${className || ""}`}
>
{children}
<div className="hidden absolute top-0 left-5 w-0.25 h-full bg-stroke-1 pointer-events-none md:block lg:left-7.5 xl:left-10"></div>
<div className="hidden absolute top-0 right-5 w-0.25 h-full bg-stroke-1 pointer-events-none md:block lg:right-7.5 xl:right-10"></div>
{crosses && (
<>
<div
className={`hidden absolute top-0 left-7.5 right-7.5 h-0.25 bg-stroke-1 ${
crossesOffset && crossesOffset
} pointer-events-none lg:block xl:left-10 right-10`}
></div>
<svg
className={`hidden absolute -top-[0.3125rem] left-[1.5625rem] ${
crossesOffset && crossesOffset
} pointer-events-none lg:block xl:left-[2.1875rem]`}
xmlns="http://www.w3.org/2000/svg"
width="11"
height="11"
fill="none"
>
<path
d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
fill="#ada8c4"
/>
</svg>
<svg
className={`hidden absolute -top-[0.3125rem] right-[1.5625rem] ${
crossesOffset && crossesOffset
} pointer-events-none lg:block xl:right-[2.1875rem]`}
xmlns="http://www.w3.org/2000/svg"
width="11"
height="11"
fill="none"
>
<path
d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
fill="#ada8c4"
/>
</svg>
</>
)}
</div>
<div
className={`relative ${
customPaddings ||
`py-10 lg:py-16 xl:py-20 ${crosses ? 'lg:py-32 xl:py-40' : ''}`
} ${className || ''}`}
>
{children}
<div className="hidden absolute top-0 left-5 w-0.25 h-full bg-stroke-1 pointer-events-none md:block lg:left-7.5 xl:left-10"></div>
<div className="hidden absolute top-0 right-5 w-0.25 h-full bg-stroke-1 pointer-events-none md:block lg:right-7.5 xl:right-10"></div>
{crosses && (
<>
<div
className={`hidden absolute top-0 left-7.5 right-7.5 h-0.25 bg-stroke-1 ${
crossesOffset && crossesOffset
} pointer-events-none lg:block xl:left-10 right-10`}
></div>
<svg
className={`hidden absolute -top-[0.3125rem] left-[1.5625rem] ${
crossesOffset && crossesOffset
} pointer-events-none lg:block xl:left-[2.1875rem]`}
xmlns="http://www.w3.org/2000/svg"
width="11"
height="11"
fill="none"
>
<path
d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
fill="#ada8c4"
/>
</svg>
<svg
className={`hidden absolute -top-[0.3125rem] right-[1.5625rem] ${
crossesOffset && crossesOffset
} pointer-events-none lg:block xl:right-[2.1875rem]`}
xmlns="http://www.w3.org/2000/svg"
width="11"
height="11"
fill="none"
>
<path
d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
fill="#ada8c4"
/>
</svg>
</>
)}
</div>
);
export default Section;

View File

@@ -1,195 +0,0 @@
import Section from "@/components/Section";
import Generating from "@/components/Generating";
import Image from "@/components/Image";
import Heading from "@/components/Heading";
type ServicesProps = {
containerClassName?: string;
};
const Services = ({ containerClassName }: ServicesProps) => (
<Section>
<div className={`container ${containerClassName || ""}`}>
<Heading
title="Generative AI made for creators."
text="Brainwave unlocks the potential of AI-powered applications"
/>
<div className="relative">
<div className="relative z-1 flex items-center h-[38.75rem] mb-5 p-8 border border-n-1/10 rounded-3xl overflow-hidden lg:h-[38.75rem] lg:p-20 xl:h-[45.75rem]">
<div className="absolute top-0 left-0 w-full h-full pointer-events-none md:w-3/5 xl:w-auto">
<Image
className="w-full h-full object-cover md:object-right"
src="/images/services/service-1.png"
width={797}
height={733}
alt="Smartest AI"
/>
</div>
<div className="relative z-1 max-w-[17rem] ml-auto">
<h4 className="h4 mb-4">Smartest AI</h4>
<p className="bpdy-2 mb-[3.125rem] text-n-3">
Brainwave unlocks the potential of AI-powered
applications
</p>
<ul className="body-2">
{[
"Photo generating",
"Photo enhance",
"Seamless Integration",
].map((item, index) => (
<li
className="flex items-start py-4 border-t border-n-6"
key={index}
>
<Image
src="/images/check.svg"
width={24}
height={24}
alt="Check"
/>
<p className="ml-4">{item}</p>
</li>
))}
</ul>
</div>
<Generating className="absolute left-4 right-4 bottom-4 border border-n-1/10 lg:left-1/2 lg-right-auto lg:bottom-8 lg:-translate-x-1/2" />
</div>
<div className="relative z-1 grid gap-5 lg:grid-cols-2">
<div className="relative min-h-[38.75rem] border border-n-1/10 rounded-3xl overflow-hidden">
<div className="absolute inset-0">
<Image
className="w-full h-full object-cover"
src="/images/services/service-2.png"
width={630}
height={748}
alt="Smartest AI"
/>
</div>
<div className="absolute inset-0 flex flex-col justify-end p-8 bg-gradient-to-b from-n-8/0 to-n-8/90 lg:p-15">
<h4 className="h4 mb-4">Photo editing</h4>
<p className="body-2 text-n-3">
{`Automatically enhance your photos using our AI app's
photo editing feature. Try it now!`}
</p>
</div>
<div className="absolute top-8 right-8 max-w-[17.5rem] py-6 px-8 bg-black rounded-t-xl rounded-bl-xl font-code text-base lg:top-16 lg:right-[8.75rem] lg:max-w-[17.5rem]">
Hey Brainwave, enhance this photo
<svg
className="absolute left-full bottom-0"
xmlns="http://www.w3.org/2000/svg"
width="26"
height="37"
>
<path d="M21.843 37.001c3.564 0 5.348-4.309 2.829-6.828L3.515 9.015A12 12 0 0 1 0 .53v36.471h21.843z" />
</svg>
</div>
</div>
<div className="p-4 bg-n-7 rounded-3xl overflow-hidden lg:min-h-[45.75rem]">
<div className="py-12 px-4 xl:px-8">
<h4 className="h4 mb-4">Video generation</h4>
<p className="body-2 mb-[2.25rem] text-n-3">
The worlds most powerful AI photo and video art
generation engine.What will you create?
</p>
<ul className="flex items-center justify-between">
{[
"/images/icons/recording-03.svg",
"/images/icons/recording-01.svg",
"/images/icons/disc-02.svg",
"/images/icons/chrome-cast.svg",
"/images/icons/sliders-04.svg",
].map((item, index) => (
<li
className={`flex items-center justify-center ${
index === 2
? "w-[3rem] h-[3rem] p-0.25 bg-conic-gradient rounded-2xl md:w-[4.5rem] md:h-[4.5rem]"
: "flex w-10 h-10 bg-n-6 rounded-2xl md:w-15 md:h-15"
}`}
key={index}
>
<div
className={
index === 2
? "flex items-center justify-center w-full h-full bg-n-7 rounded-[0.9375rem]"
: ""
}
>
<Image
src={item}
width={24}
height={24}
alt={item}
/>
</div>
</li>
))}
</ul>
</div>
<div className="relative h-[20.5rem] bg-n-8 rounded-xl overflow-hidden md:h-[25rem]">
<Image
className="w-full h-full object-cover"
src="/images/services/service-3.png"
width={517}
height={400}
alt="Smartest AI"
/>
<div className="absolute top-8 left-[3.125rem] w-full max-w-[14rem] pt-2.5 pr-2.5 pb-7 pl-5 bg-n-6 rounded-t-xl rounded-br-xl font-code text-base md:max-w-[17.5rem]">
Video generated!
<div className="absolute left-5 -bottom-[1.125rem] flex items-center justify-center w-[2.25rem] h-[2.25rem] bg-color-1 rounded-[0.75rem]">
<Image
src="/images/brainwave-symbol-white.svg"
width={26}
height={26}
alt="Brainwave"
/>
</div>
<div className="tagline absolute right-2.5 bottom-1 text-[0.625rem] text-n-3 uppercase">
just now
</div>
<svg
className="absolute right-full bottom-0 -scale-x-100"
xmlns="http://www.w3.org/2000/svg"
width="26"
height="37"
>
<path
className="fill-n-6"
d="M21.843 37.001c3.564 0 5.348-4.309 2.829-6.828L3.515 9.015A12 12 0 0 1 0 .53v36.471h21.843z"
/>
</svg>
</div>
<div className="absolute left-0 bottom-0 w-full flex items-center p-6">
<svg
className="mr-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
>
<path
d="M8.006 2.802l.036.024 10.549 7.032.805.567c.227.183.494.437.648.808a2 2 0 0 1 0 1.532c-.154.371-.421.625-.648.808-.217.175-.5.364-.805.567L8.006 21.198l-.993.627c-.285.154-.676.331-1.132.303a2 2 0 0 1-1.476-.79c-.276-.365-.346-.788-.375-1.111S4 19.502 4 19.054V4.99v-.043l.029-1.174c.03-.323.1-.746.375-1.11a2 2 0 0 1 1.476-.79c.456-.027.847.149 1.132.304s.62.378.993.627z"
fill="#fff"
/>
</svg>
<div className="flex-1 bg-[#D9D9D9]">
<div className="w-1/2 h-0.5 bg-color-1"></div>
</div>
</div>
</div>
</div>
</div>
<div className="absolute top-0 -left-[10rem] w-[56.625rem] h-[56.625rem] opacity-50 mix-blend-color-dodge pointer-events-none">
<Image
className="absolute top-1/2 left-1/2 w-[79.5625rem] max-w-[79.5625rem] h-[88.5625rem] -translate-x-1/2 -translate-y-1/2"
src="/images/gradient.png"
width={1417}
height={1417}
alt="Gradient"
/>
</div>
</div>
</div>
</Section>
);
export default Services;

View File

@@ -1,195 +0,0 @@
import Section from "@/components/Section";
import Generating from "@/components/Generating";
import Image from "@/components/Image";
import Heading from "@/components/Heading";
type ServicesProps = {
containerClassName?: string;
};
const Services = ({ containerClassName }: ServicesProps) => (
<Section>
<div className={`container ${containerClassName || ""}`}>
<Heading
title="Generative AI made for creators."
text="Brainwave unlocks the potential of AI-powered applications"
/>
<div className="relative">
<div className="relative z-1 flex items-center h-[38.75rem] mb-5 p-8 border border-n-1/10 rounded-3xl overflow-hidden lg:h-[38.75rem] lg:p-20 xl:h-[45.75rem]">
<div className="absolute top-0 left-0 w-full h-full pointer-events-none md:w-3/5 xl:w-auto">
<Image
className="w-full h-full object-cover md:object-right"
src="/images/services/service-1.png"
width={797}
height={733}
alt="Smartest AI"
/>
</div>
<div className="relative z-1 max-w-[17rem] ml-auto">
<h4 className="h4 mb-4">Smartest AI</h4>
<p className="bpdy-2 mb-[3.125rem] text-n-3">
Brainwave unlocks the potential of AI-powered
applications
</p>
<ul className="body-2">
{[
"Photo generating",
"Photo enhance",
"Seamless Integration",
].map((item, index) => (
<li
className="flex items-start py-4 border-t border-n-6"
key={index}
>
<Image
src="/images/check.svg"
width={24}
height={24}
alt="Check"
/>
<p className="ml-4">{item}</p>
</li>
))}
</ul>
</div>
<Generating className="absolute left-4 right-4 bottom-4 border border-n-1/10 lg:left-1/2 lg-right-auto lg:bottom-8 lg:-translate-x-1/2" />
</div>
<div className="relative z-1 grid gap-5 lg:grid-cols-2">
<div className="relative min-h-[38.75rem] border border-n-1/10 rounded-3xl overflow-hidden">
<div className="absolute inset-0">
<Image
className="w-full h-full object-cover"
src="/images/services/service-2.png"
width={630}
height={748}
alt="Smartest AI"
/>
</div>
<div className="absolute inset-0 flex flex-col justify-end p-8 bg-gradient-to-b from-n-8/0 to-n-8/90 lg:p-15">
<h4 className="h4 mb-4">Photo editing</h4>
<p className="body-2 text-n-3">
{`Automatically enhance your photos using our AI app's
photo editing feature. Try it now!`}
</p>
</div>
<div className="absolute top-8 right-8 max-w-[17.5rem] py-6 px-8 bg-black rounded-t-xl rounded-bl-xl font-code text-base lg:top-16 lg:right-[8.75rem] lg:max-w-[17.5rem]">
Hey Brainwave, enhance this photo
<svg
className="absolute left-full bottom-0"
xmlns="http://www.w3.org/2000/svg"
width="26"
height="37"
>
<path d="M21.843 37.001c3.564 0 5.348-4.309 2.829-6.828L3.515 9.015A12 12 0 0 1 0 .53v36.471h21.843z" />
</svg>
</div>
</div>
<div className="p-4 bg-n-7 rounded-3xl overflow-hidden lg:min-h-[45.75rem]">
<div className="py-12 px-4 xl:px-8">
<h4 className="h4 mb-4">Video generation</h4>
<p className="body-2 mb-[2.25rem] text-n-3">
The worlds most powerful AI photo and video art
generation engine.What will you create?
</p>
<ul className="flex items-center justify-between">
{[
"/images/icons/recording-03.svg",
"/images/icons/recording-01.svg",
"/images/icons/disc-02.svg",
"/images/icons/chrome-cast.svg",
"/images/icons/sliders-04.svg",
].map((item, index) => (
<li
className={`flex items-center justify-center ${
index === 2
? "w-[3rem] h-[3rem] p-0.25 bg-conic-gradient rounded-2xl md:w-[4.5rem] md:h-[4.5rem]"
: "flex w-10 h-10 bg-n-6 rounded-2xl md:w-15 md:h-15"
}`}
key={index}
>
<div
className={
index === 2
? "flex items-center justify-center w-full h-full bg-n-7 rounded-[0.9375rem]"
: ""
}
>
<Image
src={item}
width={24}
height={24}
alt={item}
/>
</div>
</li>
))}
</ul>
</div>
<div className="relative h-[20.5rem] bg-n-8 rounded-xl overflow-hidden md:h-[25rem]">
<Image
className="w-full h-full object-cover"
src="/images/services/service-3.png"
width={517}
height={400}
alt="Smartest AI"
/>
<div className="absolute top-8 left-[3.125rem] w-full max-w-[14rem] pt-2.5 pr-2.5 pb-7 pl-5 bg-n-6 rounded-t-xl rounded-br-xl font-code text-base md:max-w-[17.5rem]">
Video generated!
<div className="absolute left-5 -bottom-[1.125rem] flex items-center justify-center w-[2.25rem] h-[2.25rem] bg-color-1 rounded-[0.75rem]">
<Image
src="/images/brainwave-symbol-white.svg"
width={26}
height={26}
alt="Brainwave"
/>
</div>
<div className="tagline absolute right-2.5 bottom-1 text-[0.625rem] text-n-3 uppercase">
just now
</div>
<svg
className="absolute right-full bottom-0 -scale-x-100"
xmlns="http://www.w3.org/2000/svg"
width="26"
height="37"
>
<path
className="fill-n-6"
d="M21.843 37.001c3.564 0 5.348-4.309 2.829-6.828L3.515 9.015A12 12 0 0 1 0 .53v36.471h21.843z"
/>
</svg>
</div>
<div className="absolute left-0 bottom-0 w-full flex items-center p-6">
<svg
className="mr-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
>
<path
d="M8.006 2.802l.036.024 10.549 7.032.805.567c.227.183.494.437.648.808a2 2 0 0 1 0 1.532c-.154.371-.421.625-.648.808-.217.175-.5.364-.805.567L8.006 21.198l-.993.627c-.285.154-.676.331-1.132.303a2 2 0 0 1-1.476-.79c-.276-.365-.346-.788-.375-1.111S4 19.502 4 19.054V4.99v-.043l.029-1.174c.03-.323.1-.746.375-1.11a2 2 0 0 1 1.476-.79c.456-.027.847.149 1.132.304s.62.378.993.627z"
fill="#fff"
/>
</svg>
<div className="flex-1 bg-[#D9D9D9]">
<div className="w-1/2 h-0.5 bg-color-1"></div>
</div>
</div>
</div>
</div>
</div>
<div className="absolute top-0 -left-[10rem] w-[56.625rem] h-[56.625rem] opacity-50 mix-blend-color-dodge pointer-events-none">
<Image
className="absolute top-1/2 left-1/2 w-[79.5625rem] max-w-[79.5625rem] h-[88.5625rem] -translate-x-1/2 -translate-y-1/2"
src="/images/gradient.png"
width={1417}
height={1417}
alt="Gradient"
/>
</div>
</div>
</div>
</Section>
);
export default Services;

View File

@@ -3,7 +3,6 @@ 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';
/**
* 股票信息
@@ -72,11 +71,6 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
setError(null);
try {
logger.debug('KLineChartModal', 'loadData', '开始加载K线数据', {
stockCode: stock.stock_code,
eventTime,
});
const response = await stockService.getKlineData(
stock.stock_code,
'daily',
@@ -91,12 +85,8 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
console.log('[KLineChartModal] 数据条数:', response.data.length);
setData(response.data);
logger.info('KLineChartModal', 'loadData', 'K线数据加载成功', {
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);

View File

@@ -33,9 +33,6 @@ import {
// 工具函数
import { createSubIndicators } from './utils';
// 日志
import { logger } from '@utils/logger';
// ==================== 组件 Props ====================
export interface StockChartKLineModalProps {
@@ -110,10 +107,6 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
const handleChartTypeChange = useCallback((e: RadioChangeEvent) => {
const newType = e.target.value as ChartType;
setChartType(newType);
logger.debug('StockChartKLineModal', 'handleChartTypeChange', '切换图表类型', {
newType,
});
}, []);
/**
@@ -130,10 +123,6 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
// 先移除所有副图指标KLineChart 会自动移除)
// 然后创建新的指标
createSubIndicators(chart, values);
logger.debug('StockChartKLineModal', 'handleIndicatorChange', '切换副图指标', {
indicators: values,
});
},
[chart]
);
@@ -143,7 +132,6 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
*/
const handleRefresh = useCallback(() => {
loadData();
logger.debug('StockChartKLineModal', 'handleRefresh', '刷新数据');
}, [loadData]);
// ==================== 计算属性 ====================

View File

@@ -18,7 +18,6 @@ import {
} from '@chakra-ui/react';
import * as echarts from 'echarts';
import { stockService } from '@services/eventService';
import { logger } from '@utils/logger';
/**
* 股票信息
@@ -76,11 +75,6 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
setError(null);
try {
logger.debug('TimelineChartModal', 'loadData', '开始加载分时图数据', {
stockCode: stock.stock_code,
eventTime,
});
const response = await stockService.getKlineData(
stock.stock_code,
'timeline',
@@ -95,12 +89,8 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
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);

View File

@@ -13,7 +13,6 @@ import {
createEventHighlightOverlay,
removeAllEventMarkers,
} from '../utils/eventMarkerUtils';
import { logger } from '@utils/logger';
export interface UseEventMarkerOptions {
/** KLineChart 实例 */
@@ -77,10 +76,6 @@ export const useEventMarker = (
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;
}
@@ -93,9 +88,6 @@ export const useEventMarker = (
const overlay = createEventMarkerOverlay(eventMarker, data);
if (!overlay) {
logger.warn('useEventMarker', 'createMarker', 'Overlay 创建失败', {
eventMarker,
});
return;
}
@@ -103,9 +95,6 @@ export const useEventMarker = (
const id = chart.createOverlay(overlay);
if (!id || (Array.isArray(id) && id.length === 0)) {
logger.warn('useEventMarker', 'createMarker', '标记添加失败', {
overlay,
});
return;
}
@@ -118,23 +107,9 @@ export const useEventMarker = (
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]
@@ -150,26 +125,17 @@ export const useEventMarker = (
try {
if (markerId) {
chart.removeOverlay(markerId);
chart.removeOverlay({ id: markerId });
}
if (highlightId) {
chart.removeOverlay(highlightId);
chart.removeOverlay({ id: 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]);
@@ -186,12 +152,8 @@ export const useEventMarker = (
setMarker(null);
setMarkerId(null);
setHighlightId(null);
logger.debug('useEventMarker', 'removeAllMarkers', '移除所有事件标记和高亮', {
chartId: chart.id,
});
} catch (err) {
logger.error('useEventMarker', 'removeAllMarkers', err as Error);
// 忽略移除所有标记时的错误
}
}, [chart]);
@@ -216,10 +178,10 @@ export const useEventMarker = (
if (chart) {
try {
if (markerId) {
chart.removeOverlay(markerId);
chart.removeOverlay({ id: markerId });
}
if (highlightId) {
chart.removeOverlay(highlightId);
chart.removeOverlay({ id: highlightId });
}
} catch (err) {
// 忽略清理时的错误

View File

@@ -10,7 +10,6 @@ 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 {
@@ -65,11 +64,9 @@ export const useKLineChart = (
// 全局注册自定义均价线指标(只执行一次)
useEffect(() => {
try {
registerIndicator(avgPriceIndicator);
logger.debug('useKLineChart', '✅ 自定义均价线指标(AVG)注册成功');
registerIndicator(avgPriceIndicator as any);
} catch (err) {
// 如果已注册会报错,忽略即可
logger.debug('useKLineChart', 'AVG指标已注册或注册失败', err);
}
}, []);
@@ -78,16 +75,10 @@ export const useKLineChart = (
// 图表初始化函数
const initChart = (): boolean => {
if (!chartRef.current) {
logger.warn('useKLineChart', 'init', '图表容器未挂载,将在 50ms 后重试', { containerId });
return false;
}
try {
logger.debug('useKLineChart', 'init', '开始初始化图表', {
containerId,
height,
colorMode,
});
// 初始化图表实例KLineChart 10.0 API
// ✅ 根据 chartType 选择主题
@@ -112,29 +103,16 @@ export const useKLineChart = (
// ✅ 新增:创建成交量指标窗格
try {
const volumePaneId = chartInstance.createIndicator('VOL', false, {
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;
@@ -146,11 +124,6 @@ export const useKLineChart = (
// 成功,直接返回清理函数
return () => {
if (chartInstanceRef.current) {
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
containerId,
chartId: chartInstanceRef.current.id,
});
dispose(chartInstanceRef.current);
chartInstanceRef.current = null;
setChartInstance(null); // ✅ 新增:清空 state
@@ -161,7 +134,6 @@ export const useKLineChart = (
// 失败则延迟重试(处理 Modal 动画延迟导致的 DOM 未挂载)
const timer = setTimeout(() => {
logger.debug('useKLineChart', 'init', '执行延迟重试', { containerId });
initChart();
}, 50);
@@ -169,11 +141,6 @@ export const useKLineChart = (
return () => {
clearTimeout(timer);
if (chartInstanceRef.current) {
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
containerId,
chartId: chartInstanceRef.current.id,
});
dispose(chartInstanceRef.current);
chartInstanceRef.current = null;
setChartInstance(null); // ✅ 新增:清空 state
@@ -195,14 +162,8 @@ export const useKLineChart = (
? 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]);
@@ -215,7 +176,6 @@ export const useKLineChart = (
const handleResize = () => {
if (chartInstanceRef.current) {
chartInstanceRef.current.resize();
logger.debug('useKLineChart', 'resize', '调整图表大小');
}
};

View File

@@ -8,7 +8,6 @@ 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';
@@ -78,7 +77,6 @@ export const useKLineData = (
*/
const loadData = useCallback(async () => {
if (!stockCode) {
logger.warn('useKLineData', 'loadData', '股票代码为空', { chartType });
return;
}
@@ -86,11 +84,6 @@ export const useKLineData = (
setError(null);
try {
logger.debug('useKLineData', 'loadData', '开始加载数据', {
stockCode,
chartType,
eventTime,
});
// 1. 先检查缓存
const cacheKey = getCacheKey(stockCode, eventTime, chartType);
@@ -125,19 +118,8 @@ export const useKLineData = (
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([]);
@@ -207,9 +189,7 @@ export const useKLineData = (
(chart as any).setOffsetRightDistance(50);
}
} catch (err) {
logger.error('useKLineData', 'updateChartData', err as Error, {
step: '调整可见范围失败',
});
// 忽略调整可见范围时的错误
}
}, 100); // 延迟 100ms 确保数据已加载和渲染
@@ -259,14 +239,8 @@ export const useKLineData = (
}, 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]
@@ -279,11 +253,6 @@ export const useKLineData = (
(newData: KLineDataPoint[]) => {
setData(newData);
updateChartData(newData);
logger.debug(
'useKLineData',
`updateData - ${stockCode} (${chartType}) - ${newData.length}条数据手动更新`
);
},
[updateChartData]
);
@@ -298,7 +267,6 @@ export const useKLineData = (
if (chart) {
chart.resetData();
logger.debug('useKLineData', `clearData - chartId: ${(chart as any).id}`);
}
}, [chart]);

View File

@@ -5,17 +5,18 @@
* 计算公式:累计成交额 / 累计成交量
*/
import type { Indicator, KLineData } from 'klinecharts';
import type { KLineData } from 'klinecharts';
export const avgPriceIndicator: Indicator = {
// 使用部分类型定义,因为 Indicator 类型很复杂
export const avgPriceIndicator = {
name: 'AVG',
shortName: 'AVG',
calcParams: [],
calcParams: [] as number[],
shouldOhlc: false, // 不显示 OHLC 信息
shouldFormatBigNumber: false,
precision: 2,
minValue: null,
maxValue: null,
minValue: null as number | null,
maxValue: null as number | null,
figures: [
{
@@ -61,33 +62,27 @@ export const avgPriceIndicator: Indicator = {
},
/**
* Tooltip 格式化(显示均价 + 涨跌幅)
* 自定义 Tooltip 数据源
* 符合 IndicatorTooltipData 接口要求
*/
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}%)`;
}
const avgValue = kLineData?.avg;
const lineColor = defaultStyles?.lines?.[0]?.color || '#FF9800';
return {
title: { text: '均价', color: defaultStyles.tooltip.text.color },
value: {
text: changeText,
color: '#FF9800',
},
name: 'AVG',
calcParamsText: '',
features: [] as any[],
legends: [
{
title: { text: '均价: ', color: lineColor },
value: {
text: avgValue !== undefined ? avgValue.toFixed(2) : '--',
color: lineColor,
},
},
],
};
},
};

View File

@@ -4,8 +4,7 @@
* 包含图表初始化、技术指标管理等通用逻辑
*/
import type { Chart } from 'klinecharts';
import { logger } from '@utils/logger';
import type { Chart, ActionType } from 'klinecharts';
/**
* 安全地执行图表操作(捕获异常)
@@ -21,7 +20,6 @@ export const safeChartOperation = <T>(
try {
return fn();
} catch (error) {
logger.error('chartUtils', operation, error as Error);
return null;
}
};
@@ -50,13 +48,6 @@ export const createIndicator = (
isStack
);
logger.debug('chartUtils', 'createIndicator', '创建技术指标', {
indicatorName,
params,
isStack,
indicatorId,
});
return indicatorId;
});
};
@@ -69,8 +60,11 @@ export const createIndicator = (
*/
export const removeIndicator = (chart: Chart, indicatorId?: string): void => {
safeChartOperation('removeIndicator', () => {
chart.removeIndicator(indicatorId);
logger.debug('chartUtils', 'removeIndicator', '移除技术指标', { indicatorId });
if (indicatorId) {
chart.removeIndicator({ id: indicatorId });
} else {
chart.removeIndicator({});
}
});
};
@@ -94,11 +88,6 @@ export const createSubIndicators = (
}
});
logger.debug('chartUtils', 'createSubIndicators', '批量创建副图指标', {
indicators,
createdIds: ids,
});
return ids;
};
@@ -130,10 +119,6 @@ export const setChartZoom = (chart: Chart, zoom: number): void => {
},
});
logger.debug('chartUtils', 'setChartZoom', '设置图表缩放', {
zoom,
newBarSpace,
});
});
};
@@ -147,8 +132,6 @@ export const scrollToTimestamp = (chart: Chart, timestamp: number): void => {
safeChartOperation('scrollToTimestamp', () => {
// KLineChart 10.0: 使用 scrollToTimestamp 方法
chart.scrollToTimestamp(timestamp);
logger.debug('chartUtils', 'scrollToTimestamp', '滚动到指定时间', { timestamp });
});
};
@@ -160,7 +143,6 @@ export const scrollToTimestamp = (chart: Chart, timestamp: number): void => {
export const resizeChart = (chart: Chart): void => {
safeChartOperation('resizeChart', () => {
chart.resize();
logger.debug('chartUtils', 'resizeChart', '调整图表大小');
});
};
@@ -194,7 +176,6 @@ export const getVisibleRange = (chart: Chart): { from: number; to: number } | nu
export const clearChartData = (chart: Chart): void => {
safeChartOperation('clearChartData', () => {
chart.resetData();
logger.debug('chartUtils', 'clearChartData', '清空图表数据');
});
};
@@ -213,11 +194,6 @@ export const exportChartImage = (
// KLineChart 10.0: 使用 getConvertPictureUrl 方法
const imageData = chart.getConvertPictureUrl(includeOverlay, 'png', '#ffffff');
logger.debug('chartUtils', 'exportChartImage', '导出图表图片', {
includeOverlay,
hasData: !!imageData,
});
return imageData;
});
};
@@ -235,8 +211,6 @@ export const toggleCrosshair = (chart: Chart, show: boolean): void => {
show,
},
});
logger.debug('chartUtils', 'toggleCrosshair', '切换十字光标', { show });
});
};
@@ -253,8 +227,6 @@ export const toggleGrid = (chart: Chart, show: boolean): void => {
show,
},
});
logger.debug('chartUtils', 'toggleGrid', '切换网格', { show });
});
};
@@ -267,12 +239,11 @@ export const toggleGrid = (chart: Chart, show: boolean): void => {
*/
export const subscribeChartEvent = (
chart: Chart,
eventName: string,
eventName: ActionType,
handler: (...args: any[]) => void
): void => {
safeChartOperation(`subscribeChartEvent:${eventName}`, () => {
chart.subscribeAction(eventName, handler);
logger.debug('chartUtils', 'subscribeChartEvent', '订阅图表事件', { eventName });
});
};
@@ -285,11 +256,10 @@ export const subscribeChartEvent = (
*/
export const unsubscribeChartEvent = (
chart: Chart,
eventName: string,
eventName: ActionType,
handler: (...args: any[]) => void
): void => {
safeChartOperation(`unsubscribeChartEvent:${eventName}`, () => {
chart.unsubscribeAction(eventName, handler);
logger.debug('chartUtils', 'unsubscribeChartEvent', '取消订阅图表事件', { eventName });
});
};

View File

@@ -6,7 +6,6 @@
import dayjs from 'dayjs';
import type { KLineDataPoint, RawDataPoint, ChartType } from '../types';
import { logger } from '@utils/logger';
/**
* 将后端原始数据转换为 KLineChart 标准格式
@@ -22,7 +21,6 @@ export const convertToKLineData = (
eventTime?: string
): KLineDataPoint[] => {
if (!rawData || !Array.isArray(rawData) || rawData.length === 0) {
logger.warn('dataAdapter', 'convertToKLineData', '原始数据为空', { chartType });
return [];
}
@@ -37,15 +35,11 @@ export const convertToKLineData = (
low: Number(item.low) || 0,
close: Number(item.close) || 0,
volume: Number(item.volume) || 0,
turnover: item.turnover ? Number(item.turnover) : undefined,
turnover: (item as any).turnover ? Number((item as any).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 [];
}
};
@@ -90,7 +84,6 @@ const parseTimestamp = (
}
// 默认返回当前时间(避免图表崩溃)
logger.warn('dataAdapter', 'parseTimestamp', '无法解析时间戳,使用当前时间', { item });
return Date.now();
};
@@ -109,7 +102,6 @@ const parseTimelineTimestamp = (time: string, eventTime: string): 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();
}
};
@@ -126,19 +118,16 @@ 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;
}
@@ -213,17 +202,8 @@ export const trimDataByEventTime = (
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; // 出错时返回原始数据
}
};
@@ -260,13 +240,6 @@ export const processChartData = (
data = trimDataByEventTime(data, eventTime, chartType);
}
logger.debug('dataAdapter', 'processChartData', '数据处理完成', {
rawLength: rawData.length,
processedLength: data.length,
chartType,
hasEventTime: !!eventTime,
});
return data;
};

View File

@@ -9,7 +9,6 @@ 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 格式)
@@ -27,10 +26,6 @@ export const createEventMarkerOverlay = (
const closestPoint = findClosestDataPoint(data, marker.timestamp);
if (!closestPoint) {
logger.warn('eventMarkerUtils', 'createEventMarkerOverlay', '未找到匹配的数据点', {
markerId: marker.id,
timestamp: marker.timestamp,
});
return null;
}
@@ -64,10 +59,6 @@ export const createEventMarkerOverlay = (
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,
},
},
// 标记文本内容
@@ -77,17 +68,8 @@ export const createEventMarkerOverlay = (
},
};
logger.debug('eventMarkerUtils', 'createEventMarkerOverlay', '创建事件标记', {
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;
}
};
@@ -108,7 +90,6 @@ export const createEventHighlightOverlay = (
const closestPoint = findClosestDataPoint(data, eventTimestamp);
if (!closestPoint) {
logger.warn('eventMarkerUtils', 'createEventHighlightOverlay', '未找到匹配的数据点');
return null;
}
@@ -135,14 +116,8 @@ export const createEventHighlightOverlay = (
},
};
logger.debug('eventMarkerUtils', 'createEventHighlightOverlay', '创建事件高亮覆盖层', {
timestamp: closestPoint.timestamp,
eventTime,
});
return overlay;
} catch (error) {
logger.error('eventMarkerUtils', 'createEventHighlightOverlay', error as Error);
return null;
}
};
@@ -219,11 +194,6 @@ export const createEventMarkerOverlays = (
}
});
logger.debug('eventMarkerUtils', 'createEventMarkerOverlays', '批量创建事件标记', {
totalMarkers: markers.length,
createdOverlays: overlays.length,
});
return overlays;
};
@@ -235,10 +205,9 @@ export const createEventMarkerOverlays = (
*/
export const removeEventMarker = (chart: any, markerId: string): void => {
try {
chart.removeOverlay(markerId);
logger.debug('eventMarkerUtils', 'removeEventMarker', '移除事件标记', { markerId });
chart.removeOverlay({ id: markerId });
} catch (error) {
logger.error('eventMarkerUtils', 'removeEventMarker', error as Error, { markerId });
// 忽略移除标记时的错误
}
};
@@ -251,9 +220,8 @@ export const removeAllEventMarkers = (chart: any): void => {
try {
// KLineChart 10.0 API: removeOverlay() 不传参数时移除所有 overlays
chart.removeOverlay();
logger.debug('eventMarkerUtils', 'removeAllEventMarkers', '移除所有事件标记');
} catch (error) {
logger.error('eventMarkerUtils', 'removeAllEventMarkers', error as Error);
// 忽略移除所有标记时的错误
}
};
@@ -275,13 +243,8 @@ export const updateEventMarker = (
// 重新创建标记KLineChart 10.0 不支持直接更新 overlay
// 注意:需要在调用方重新创建并添加 overlay
logger.debug('eventMarkerUtils', 'updateEventMarker', '更新事件标记', {
markerId,
updates,
});
} catch (error) {
logger.error('eventMarkerUtils', 'updateEventMarker', error as Error, { markerId });
// 忽略更新标记时的错误
}
};
@@ -309,12 +272,8 @@ export const highlightEventMarker = (
},
});
logger.debug('eventMarkerUtils', 'highlightEventMarker', '高亮事件标记', {
markerId,
highlight,
});
} catch (error) {
logger.error('eventMarkerUtils', 'highlightEventMarker', error as Error, { markerId });
// 忽略高亮标记时的错误
}
};

View File

@@ -0,0 +1,66 @@
// src/constants/homeFeatures.ts
// 首页功能特性配置
import type { Feature } from '@/types/home';
/**
* 核心功能特性列表
* 第一个功能为特色功能,会以突出样式显示
*/
export const CORE_FEATURES: Feature[] = [
{
id: 'news-catalyst',
title: '新闻中心',
description: '实时新闻事件分析,捕捉市场催化因子',
icon: '📊',
color: 'yellow',
url: '/community',
badge: '核心',
featured: true
},
{
id: 'concepts',
title: '概念中心',
description: '热门概念与主题投资分析追踪',
icon: '🎯',
color: 'purple',
url: '/concepts',
badge: '热门'
},
{
id: 'stocks',
title: '个股信息汇总',
description: '全面的个股基本面信息整合',
icon: '📈',
color: 'blue',
url: '/stocks',
badge: '全面'
},
{
id: 'limit-analyse',
title: '涨停板块分析',
description: '涨停板数据深度分析与规律挖掘',
icon: '🚀',
color: 'green',
url: '/limit-analyse',
badge: '精准'
},
{
id: 'company',
title: '个股罗盘',
description: '个股全方位分析与投资决策支持',
icon: '🧭',
color: 'orange',
url: '/company?scode=688256',
badge: '专业'
},
{
id: 'trading-simulation',
title: '模拟盘交易',
description: '100万起始资金,体验真实交易环境',
icon: '💰',
color: 'teal',
url: '/trading-simulation',
badge: '实战'
}
];

View File

@@ -2,11 +2,61 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useToast } from '@chakra-ui/react';
import { logger } from '../utils/logger';
import { useNotification } from '../contexts/NotificationContext';
import { identifyUser, resetUser, trackEvent } from '@lib/posthog';
import { logger } from '@utils/logger';
import { performanceMonitor } from '@utils/performanceMonitor';
import { useNotification } from '@contexts/NotificationContext';
// ⚡ PostHog 延迟加载:移除同步导入,首屏减少 ~180KB
// import { identifyUser, resetUser, trackEvent } from '@lib/posthog';
import { SPECIAL_EVENTS } from '@lib/constants';
// ⚡ PostHog 延迟加载模块(动态导入后缓存)
let posthogModule = null;
/**
* ⚡ 延迟加载 PostHog 模块
*/
const loadPostHogModule = async () => {
if (posthogModule) return posthogModule;
try {
posthogModule = await import('@lib/posthog');
return posthogModule;
} catch (error) {
logger.error('AuthContext', 'PostHog 模块加载失败', error);
return null;
}
};
/**
* ⚡ 延迟调用 identifyUser
*/
const identifyUserLazy = async (userId, userProperties) => {
const module = await loadPostHogModule();
if (module) {
module.identifyUser(userId, userProperties);
}
};
/**
* ⚡ 延迟调用 resetUser
*/
const resetUserLazy = async () => {
const module = await loadPostHogModule();
if (module) {
module.resetUser();
}
};
/**
* ⚡ 延迟调用 trackEvent使用异步版本
*/
const trackEventLazy = async (eventName, properties) => {
const module = await loadPostHogModule();
if (module) {
module.trackEventAsync(eventName, properties);
}
};
// 创建认证上下文
const AuthContext = createContext();
@@ -37,6 +87,9 @@ export const AuthProvider = ({ children }) => {
// 检查Session状态
const checkSession = async () => {
// ⚡ 性能标记:认证检查开始
performanceMonitor.mark('auth-check-start');
// 节流检查
const now = Date.now();
const timeSinceLastCheck = now - lastCheckTimeRef.current;
@@ -47,6 +100,8 @@ export const AuthProvider = ({ children }) => {
minInterval: `${MIN_CHECK_INTERVAL}ms`,
reason: '距离上次请求间隔太短'
});
// ⚡ 性能标记:认证检查结束(节流情况)
performanceMonitor.mark('auth-check-end');
return;
}
@@ -93,8 +148,8 @@ export const AuthProvider = ({ children }) => {
return prevUser;
}
// ✅ 识别用户身份到 PostHog
identifyUser(data.user.id, {
// ✅ 识别用户身份到 PostHog(延迟加载)
identifyUserLazy(data.user.id, {
email: data.user.email,
username: data.user.username,
subscription_tier: data.user.subscription_tier,
@@ -125,6 +180,8 @@ export const AuthProvider = ({ children }) => {
setUser((prev) => prev === null ? prev : null);
setIsAuthenticated((prev) => prev === false ? prev : false);
} finally {
// ⚡ 性能标记:认证检查结束
performanceMonitor.mark('auth-check-end');
// ⚡ 只在 isLoading 为 true 时才设置为 false避免不必要的状态更新
setIsLoading((prev) => prev === false ? prev : false);
}
@@ -346,8 +403,8 @@ export const AuthProvider = ({ children }) => {
credentials: 'include'
});
// ✅ 追踪登出事件(必须在 resetUser() 之前,否则会丢失用户身份
trackEvent(SPECIAL_EVENTS.USER_LOGGED_OUT, {
// ✅ 追踪登出事件(延迟加载,必须在 resetUser() 之前)
trackEventLazy(SPECIAL_EVENTS.USER_LOGGED_OUT, {
timestamp: new Date().toISOString(),
user_id: user?.id || null,
session_duration_minutes: user?.session_start
@@ -355,8 +412,8 @@ export const AuthProvider = ({ children }) => {
: null,
});
// ✅ 重置 PostHog 用户会话
resetUser();
// ✅ 重置 PostHog 用户会话(延迟加载)
resetUserLazy();
// 清除本地状态
setUser(null);

View File

@@ -9,6 +9,8 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
import { useToast, Box, HStack, Text, Button, CloseButton, VStack, Icon } from '@chakra-ui/react';
import { BellIcon } from '@chakra-ui/icons';
import { useSelector } from 'react-redux';
import { selectIsMobile } from '@/store/slices/deviceSlice';
import { logger } from '../utils/logger';
import socket from '../services/socket';
import notificationSound from '../assets/sounds/notification.wav';
@@ -27,6 +29,9 @@ const CONNECTION_STATUS = {
RECONNECTED: 'reconnected', // 重连成功显示2秒后自动变回 CONNECTED
};
// ⚡ 模块级变量:防止 React Strict Mode 导致的重复初始化
let socketInitialized = false;
// 创建通知上下文
const NotificationContext = createContext();
@@ -41,6 +46,10 @@ export const useNotification = () => {
// 通知提供者组件
export const NotificationProvider = ({ children }) => {
// ⚡ 移动端检测(使用 Redux 状态)
const isMobile = useSelector(selectIsMobile);
// ========== 所有 Hooks 必须在条件判断之前调用React 规则) ==========
const toast = useToast();
const [notifications, setNotifications] = useState([]);
const [isConnected, setIsConnected] = useState(false);
@@ -562,8 +571,8 @@ export const NotificationProvider = ({ children }) => {
logger.info('NotificationContext', 'Auto-requesting browser permission on notification');
await requestBrowserPermission();
}
// 如果权限是denied已拒绝提供设置指引
else if (browserPermission === 'denied') {
// 如果权限是denied已拒绝提供设置指引(仅 PC 端显示)
else if (browserPermission === 'denied' && !isMobile) {
const toastId = 'browser-permission-denied-guide';
if (!toast.isActive(toastId)) {
toast({
@@ -649,183 +658,223 @@ export const NotificationProvider = ({ children }) => {
}, [adaptEventToNotification]);
// ========== 连接到 Socket 服务(⚡ 方案2: 只执行一次 ==========
// ========== 连接到 Socket 服务(⚡ 异步初始化,不阻塞首屏 ==========
useEffect(() => {
logger.info('NotificationContext', '初始化 Socket 连接方案2只注册一次');
// ⚡ 防止 React Strict Mode 导致的重复初始化
if (socketInitialized) {
logger.debug('NotificationContext', 'Socket 已初始化跳过重复执行Strict Mode 保护)');
return;
}
// ========== 监听连接成功(首次连接 + 重连) ==========
socket.on('connect', () => {
setIsConnected(true);
setReconnectAttempt(0);
let cleanupCalled = false;
let idleCallbackId;
let timeoutId;
// 判断是首次连接还是重连
if (isFirstConnect.current) {
logger.info('NotificationContext', '首次连接成功', {
socketId: socket.getSocketId?.()
});
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
isFirstConnect.current = false;
} else {
logger.info('NotificationContext', '重连成功');
setConnectionStatus(CONNECTION_STATUS.RECONNECTED);
// ⚡ Socket 初始化函数(将在浏览器空闲时执行)
const initSocketConnection = () => {
if (cleanupCalled || socketInitialized) return; // 防止组件卸载后执行或重复初始化
// 清除之前的定时器
if (reconnectedTimerRef.current) {
clearTimeout(reconnectedTimerRef.current);
socketInitialized = true; // 标记已初始化
logger.info('NotificationContext', '初始化 Socket 连接(异步执行,不阻塞首屏)');
// ========== 监听连接成功(首次连接 + 重连) ==========
socket.on('connect', () => {
setIsConnected(true);
setReconnectAttempt(0);
// 判断是首次连接还是重连
if (isFirstConnect.current) {
logger.info('NotificationContext', '首次连接成功', {
socketId: socket.getSocketId?.()
});
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
isFirstConnect.current = false;
} else {
logger.info('NotificationContext', '重连成功');
setConnectionStatus(CONNECTION_STATUS.RECONNECTED);
// 清除之前的定时器
if (reconnectedTimerRef.current) {
clearTimeout(reconnectedTimerRef.current);
}
// 2秒后自动变回 CONNECTED
reconnectedTimerRef.current = setTimeout(() => {
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status');
}, 2000);
}
// 2秒后自动变回 CONNECTED
reconnectedTimerRef.current = setTimeout(() => {
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status');
}, 2000);
}
// ⚡ 重连后只需重新订阅,不需要重新注册监听器
// 使用 setTimeout(0) 确保 socketService 内部状态已同步
setTimeout(() => {
logger.info('NotificationContext', '重新订阅事件推送');
// ⚡ 重连后只需重新订阅,不需要重新注册监听器
logger.info('NotificationContext', '重新订阅事件推送');
if (socket.subscribeToEvents) {
socket.subscribeToEvents({
eventType: 'all',
importance: 'all',
onSubscribed: (data) => {
logger.info('NotificationContext', '订阅成功', data);
},
});
} else {
logger.error('NotificationContext', 'socket.subscribeToEvents 方法不可用');
}
});
// ========== 监听断开连接 ==========
socket.on('disconnect', (reason) => {
setIsConnected(false);
setConnectionStatus(CONNECTION_STATUS.DISCONNECTED);
logger.warn('NotificationContext', 'Socket 已断开', { reason });
});
// ========== 监听连接错误 ==========
socket.on('connect_error', (error) => {
logger.error('NotificationContext', 'Socket connect_error', error);
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
const attempts = socket.getReconnectAttempts?.() || 0;
setReconnectAttempt(attempts);
logger.info('NotificationContext', `重连中... (第 ${attempts} 次尝试)`);
});
// ========== 监听重连失败 ==========
socket.on('reconnect_failed', () => {
logger.error('NotificationContext', '重连失败');
setConnectionStatus(CONNECTION_STATUS.FAILED);
toast({
title: '连接失败',
description: '无法连接到服务器,请检查网络连接',
status: 'error',
duration: null,
isClosable: true,
});
});
// ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数) ==========
socket.on('new_event', (data) => {
logger.info('NotificationContext', '收到 new_event 事件', {
id: data?.id,
title: data?.title,
eventType: data?.event_type || data?.type,
importance: data?.importance
});
logger.debug('NotificationContext', '原始事件数据', data);
// ⚠️ 防御性检查:确保 ref 已初始化
if (!addNotificationRef.current || !adaptEventToNotificationRef.current) {
logger.error('NotificationContext', 'Ref 未初始化,跳过处理', {
addNotificationRef: !!addNotificationRef.current,
adaptEventToNotificationRef: !!adaptEventToNotificationRef.current,
});
return;
}
// ========== Socket层去重检查 ==========
const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
if (!data.id) {
logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
eventId,
eventType: data.type,
title: data.title,
});
}
if (processedEventIds.current.has(eventId)) {
logger.warn('NotificationContext', '重复事件已忽略', { eventId });
return;
}
processedEventIds.current.add(eventId);
logger.debug('NotificationContext', '事件已记录,防止重复处理', { eventId });
// 限制 Set 大小,避免内存泄漏
if (processedEventIds.current.size > MAX_PROCESSED_IDS) {
const idsArray = Array.from(processedEventIds.current);
processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS));
logger.debug('NotificationContext', 'Cleaned up old processed event IDs', {
kept: MAX_PROCESSED_IDS,
});
}
// ========== Socket层去重检查结束 ==========
// ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱)
logger.debug('NotificationContext', '正在转换事件格式');
const notification = adaptEventToNotificationRef.current(data);
logger.debug('NotificationContext', '转换后的通知对象', notification);
// ✅ 使用 ref.current 访问最新的 addNotification 函数
logger.debug('NotificationContext', '准备添加通知到队列');
addNotificationRef.current(notification);
logger.info('NotificationContext', '通知已添加到队列');
// ⚡ 调用所有注册的事件更新回调(用于通知其他组件刷新数据)
if (eventUpdateCallbacks.current.size > 0) {
logger.debug('NotificationContext', `触发 ${eventUpdateCallbacks.current.size} 个事件更新回调`);
eventUpdateCallbacks.current.forEach(callback => {
try {
callback(data);
} catch (error) {
logger.error('NotificationContext', '事件更新回调执行失败', error);
if (socket.subscribeToEvents) {
socket.subscribeToEvents({
eventType: 'all',
importance: 'all',
onSubscribed: (data) => {
logger.info('NotificationContext', '订阅成功', data);
},
});
} else {
logger.error('NotificationContext', 'socket.subscribeToEvents 方法不可用');
}
}, 0);
});
// ========== 监听断开连接 ==========
socket.on('disconnect', (reason) => {
setIsConnected(false);
setConnectionStatus(CONNECTION_STATUS.DISCONNECTED);
logger.warn('NotificationContext', 'Socket 已断开', { reason });
});
// ========== 监听连接错误 ==========
socket.on('connect_error', (error) => {
logger.error('NotificationContext', 'Socket connect_error', error);
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
const attempts = socket.getReconnectAttempts?.() || 0;
setReconnectAttempt(attempts);
logger.info('NotificationContext', `重连中... (第 ${attempts} 次尝试)`);
});
// ========== 监听重连失败 ==========
socket.on('reconnect_failed', () => {
logger.error('NotificationContext', '重连失败');
setConnectionStatus(CONNECTION_STATUS.FAILED);
toast({
title: '连接失败',
description: '无法连接到服务器,请检查网络连接',
status: 'error',
duration: null,
isClosable: true,
});
logger.debug('NotificationContext', '所有事件更新回调已触发');
}
});
});
// ========== 监听系统通知(兼容性 ==========
socket.on('system_notification', (data) => {
logger.info('NotificationContext', '收到系统通知', data);
// ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数 ==========
socket.on('new_event', (data) => {
logger.info('NotificationContext', '收到 new_event 事件', {
id: data?.id,
title: data?.title,
eventType: data?.event_type || data?.type,
importance: data?.importance
});
logger.debug('NotificationContext', '原始事件数据', data);
if (addNotificationRef.current) {
addNotificationRef.current(data);
} else {
logger.error('NotificationContext', 'addNotificationRef 未初始化');
}
});
// ⚠️ 防御性检查:确保 ref 已初始化
if (!addNotificationRef.current || !adaptEventToNotificationRef.current) {
logger.error('NotificationContext', 'Ref 未初始化,跳过处理', {
addNotificationRef: !!addNotificationRef.current,
adaptEventToNotificationRef: !!adaptEventToNotificationRef.current,
});
return;
}
logger.info('NotificationContext', '所有监听器已注册(只注册一次)');
// ========== Socket层去重检查 ==========
const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// ========== 获取最大重连次数 ==========
const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity;
setMaxReconnectAttempts(maxAttempts);
logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts });
if (!data.id) {
logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
eventId,
eventType: data.type,
title: data.title,
});
}
// ========== 启动连接 ==========
logger.info('NotificationContext', '调用 socket.connect()');
socket.connect();
if (processedEventIds.current.has(eventId)) {
logger.warn('NotificationContext', '重复事件已忽略', { eventId });
return;
}
processedEventIds.current.add(eventId);
logger.debug('NotificationContext', '事件已记录,防止重复处理', { eventId });
// 限制 Set 大小,避免内存泄漏
if (processedEventIds.current.size > MAX_PROCESSED_IDS) {
const idsArray = Array.from(processedEventIds.current);
processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS));
logger.debug('NotificationContext', 'Cleaned up old processed event IDs', {
kept: MAX_PROCESSED_IDS,
});
}
// ========== Socket层去重检查结束 ==========
// ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱)
logger.debug('NotificationContext', '正在转换事件格式');
const notification = adaptEventToNotificationRef.current(data);
logger.debug('NotificationContext', '转换后的通知对象', notification);
// ✅ 使用 ref.current 访问最新的 addNotification 函数
logger.debug('NotificationContext', '准备添加通知到队列');
addNotificationRef.current(notification);
logger.info('NotificationContext', '通知已添加到队列');
// ⚡ 调用所有注册的事件更新回调(用于通知其他组件刷新数据)
if (eventUpdateCallbacks.current.size > 0) {
logger.debug('NotificationContext', `触发 ${eventUpdateCallbacks.current.size} 个事件更新回调`);
eventUpdateCallbacks.current.forEach(callback => {
try {
callback(data);
} catch (error) {
logger.error('NotificationContext', '事件更新回调执行失败', error);
}
});
logger.debug('NotificationContext', '所有事件更新回调已触发');
}
});
// ========== 监听系统通知(兼容性) ==========
socket.on('system_notification', (data) => {
logger.info('NotificationContext', '收到系统通知', data);
if (addNotificationRef.current) {
addNotificationRef.current(data);
} else {
logger.error('NotificationContext', 'addNotificationRef 未初始化');
}
});
logger.info('NotificationContext', '所有监听器已注册');
// ========== 获取最大重连次数 ==========
const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity;
setMaxReconnectAttempts(maxAttempts);
logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts });
// ========== 启动连接 ==========
logger.info('NotificationContext', '调用 socket.connect()');
socket.connect();
};
// ⚡ 使用 requestIdleCallback 在浏览器空闲时初始化 Socket
// 降级到 setTimeout(0) 以兼容不支持的浏览器(如 Safari
if ('requestIdleCallback' in window) {
idleCallbackId = window.requestIdleCallback(initSocketConnection, {
timeout: 3000 // 最多等待 3 秒,确保连接不会延迟太久
});
logger.debug('NotificationContext', 'Socket 初始化已排入 requestIdleCallback');
} else {
timeoutId = setTimeout(initSocketConnection, 0);
logger.debug('NotificationContext', 'Socket 初始化已排入 setTimeout(0)(降级模式)');
}
// ========== 清理函数(组件卸载时) ==========
return () => {
cleanupCalled = true;
logger.info('NotificationContext', '清理 Socket 连接');
// 取消待执行的初始化
if (idleCallbackId && 'cancelIdleCallback' in window) {
window.cancelIdleCallback(idleCallbackId);
}
if (timeoutId) {
clearTimeout(timeoutId);
}
// 清理 reconnected 状态定时器
if (reconnectedTimerRef.current) {
clearTimeout(reconnectedTimerRef.current);
@@ -966,6 +1015,39 @@ export const NotificationProvider = ({ children }) => {
};
}, [browserPermission, toast]);
// ⚡ 移动端禁用完整通知能力:返回空壳 Provider
// 注意:此判断必须在所有 Hooks 之后React 规则要求 Hooks 调用顺序一致)
if (isMobile) {
const emptyValue = {
notifications: [],
isConnected: false,
soundEnabled: false,
browserPermission: 'default',
connectionStatus: CONNECTION_STATUS.DISCONNECTED,
reconnectAttempt: 0,
maxReconnectAttempts: 0,
addNotification: () => null,
removeNotification: () => {},
clearAllNotifications: () => {},
toggleSound: () => {},
requestBrowserPermission: () => Promise.resolve('default'),
trackNotificationClick: () => {},
retryConnection: () => {},
showWelcomeGuide: () => {},
showCommunityGuide: () => {},
showFirstFollowGuide: () => {},
registerEventUpdateCallback: () => () => {},
unregisterEventUpdateCallback: () => {},
};
return (
<NotificationContext.Provider value={emptyValue}>
{children}
</NotificationContext.Provider>
);
}
// ========== 桌面端:完整通知功能 ==========
const value = {
notifications,
isConnected,

View File

@@ -82,7 +82,6 @@ export const useAuthEvents = ({ component = 'AuthFormContent', isMobile = false
...getBaseProperties(),
source,
});
logger.debug('useAuthEvents', '💬 WeChat Login Initiated', { source });
}, [track, getBaseProperties]);
// ==================== 手机验证码流程 ====================
@@ -186,7 +185,6 @@ export const useAuthEvents = ({ component = 'AuthFormContent', isMobile = false
session_id: sessionId?.substring(0, 8) + '...',
has_auth_url: Boolean(authUrl),
});
logger.debug('useAuthEvents', '🔲 WeChat QR Code Displayed', { sessionId: sessionId?.substring(0, 8) });
}, [track, getBaseProperties]);
/**
@@ -198,7 +196,6 @@ export const useAuthEvents = ({ component = 'AuthFormContent', isMobile = false
...getBaseProperties(),
session_id: sessionId?.substring(0, 8) + '...',
});
logger.debug('useAuthEvents', '📱 WeChat QR Code Scanned', { sessionId: sessionId?.substring(0, 8) });
}, [track, getBaseProperties]);
/**
@@ -212,7 +209,6 @@ export const useAuthEvents = ({ component = 'AuthFormContent', isMobile = false
session_id: sessionId?.substring(0, 8) + '...',
time_elapsed: timeElapsed,
});
logger.debug('useAuthEvents', '⏰ WeChat QR Code Expired', { sessionId: sessionId?.substring(0, 8), timeElapsed });
}, [track, getBaseProperties]);
/**
@@ -226,7 +222,6 @@ export const useAuthEvents = ({ component = 'AuthFormContent', isMobile = false
old_session_id: oldSessionId?.substring(0, 8) + '...',
new_session_id: newSessionId?.substring(0, 8) + '...',
});
logger.debug('useAuthEvents', '🔄 WeChat QR Code Refreshed');
}, [track, getBaseProperties]);
/**
@@ -242,7 +237,6 @@ export const useAuthEvents = ({ component = 'AuthFormContent', isMobile = false
old_status: oldStatus,
new_status: newStatus,
});
logger.debug('useAuthEvents', '🔄 WeChat Status Changed', { oldStatus, newStatus });
}, [track, getBaseProperties]);
/**
@@ -250,7 +244,6 @@ export const useAuthEvents = ({ component = 'AuthFormContent', isMobile = false
*/
const trackWechatH5Redirect = useCallback(() => {
track(ACTIVATION_EVENTS.WECHAT_H5_REDIRECT, getBaseProperties());
logger.debug('useAuthEvents', '🔗 WeChat H5 Redirect');
}, [track, getBaseProperties]);
// ==================== 登录/注册结果 ====================

35
src/hooks/useDevice.js Normal file
View File

@@ -0,0 +1,35 @@
/**
* useDevice Hook
*
* 封装设备类型检测,提供简洁的 API 供组件使用
*
* @example
* const { isMobile, isTablet, isDesktop, deviceType } = useDevice();
*
* if (isMobile) return <MobileView />;
* if (isTablet) return <TabletView />;
* return <DesktopView />;
*/
import { useSelector } from 'react-redux';
import {
selectIsMobile,
selectIsTablet,
selectIsDesktop,
selectDeviceType,
} from '@/store/slices/deviceSlice';
export const useDevice = () => {
const isMobile = useSelector(selectIsMobile);
const isTablet = useSelector(selectIsTablet);
const isDesktop = useSelector(selectIsDesktop);
const deviceType = useSelector(selectDeviceType);
return {
isMobile,
isTablet,
isDesktop,
deviceType,
};
};
export default useDevice;

View File

@@ -0,0 +1,57 @@
// src/hooks/useHomeResponsive.ts
// 首页响应式配置 Hook
import { useBreakpointValue } from '@chakra-ui/react';
import type { ResponsiveConfig } from '@/types/home';
/**
* 首页响应式配置 Hook
* 集中管理所有响应式断点值
*
* @returns 响应式配置对象
*/
export const useHomeResponsive = (): ResponsiveConfig => {
const heroHeight = useBreakpointValue({
base: '60vh',
md: '80vh',
lg: '100vh'
});
const headingSize = useBreakpointValue({
base: 'xl',
md: '3xl',
lg: '4xl'
});
const headingLetterSpacing = useBreakpointValue({
base: '-1px',
md: '-1.5px',
lg: '-2px'
});
const heroTextSize = useBreakpointValue({
base: 'md',
md: 'lg',
lg: 'xl'
});
const containerPx = useBreakpointValue({
base: 10,
md: 10,
lg: 10
});
const showDecorations = useBreakpointValue({
base: false,
md: true
});
return {
heroHeight,
headingSize,
headingLetterSpacing,
heroTextSize,
containerPx,
showDecorations
};
};

View File

@@ -12,6 +12,7 @@ import {
resetToFree,
selectSubscriptionInfo,
selectSubscriptionLoading,
selectSubscriptionLoaded,
selectSubscriptionError,
selectSubscriptionModalOpen
} from '../store/slices/subscriptionSlice';
@@ -66,21 +67,24 @@ export const useSubscription = () => {
// Redux 状态
const subscriptionInfo = useSelector(selectSubscriptionInfo);
const loading = useSelector(selectSubscriptionLoading);
const loaded = useSelector(selectSubscriptionLoaded);
const error = useSelector(selectSubscriptionError);
const isSubscriptionModalOpen = useSelector(selectSubscriptionModalOpen);
// 自动加载订阅信息
// 自动加载订阅信息(带防重复逻辑)
useEffect(() => {
if (isAuthenticated && user) {
// 用户已登录,加载订阅信息
dispatch(fetchSubscriptionInfo());
logger.debug('useSubscription', '加载订阅信息', { userId: user.id });
// 只在未加载且未在加载中时才请求,避免多个组件重复调用
if (!loaded && !loading) {
dispatch(fetchSubscriptionInfo());
logger.debug('useSubscription', '加载订阅信息', { userId: user.id });
}
} else {
// 用户未登录,重置为免费版
dispatch(resetToFree());
logger.debug('useSubscription', '用户未登录,重置为免费版');
}
}, [isAuthenticated, user, dispatch]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated, user?.id, dispatch, loaded, loading]);
// 获取订阅级别数值
const getSubscriptionLevel = (type = null) => {

View File

@@ -1,394 +0,0 @@
// src/hooks/useSubscriptionEvents.js
// 订阅和支付事件追踪 Hook
import { useCallback } from 'react';
import { usePostHogTrack } from './usePostHogRedux';
import { RETENTION_EVENTS, REVENUE_EVENTS } from '../lib/constants';
import { logger } from '../utils/logger';
/**
* 订阅和支付事件追踪 Hook
* @param {Object} options - 配置选项
* @param {Object} options.currentSubscription - 当前订阅信息
* @returns {Object} 事件追踪处理函数集合
*/
export const useSubscriptionEvents = ({ currentSubscription = null } = {}) => {
const { track } = usePostHogTrack();
/**
* 追踪付费墙展示
* @param {string} feature - 被限制的功能名称
* @param {string} requiredPlan - 需要的订阅计划
* @param {string} triggerLocation - 触发位置
*/
const trackPaywallShown = useCallback((feature, requiredPlan = 'pro', triggerLocation = '') => {
if (!feature) {
logger.warn('useSubscriptionEvents', 'trackPaywallShown: feature is required');
return;
}
track(REVENUE_EVENTS.PAYWALL_SHOWN, {
feature,
required_plan: requiredPlan,
current_plan: currentSubscription?.plan || 'free',
trigger_location: triggerLocation,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🚧 Paywall Shown', {
feature,
requiredPlan,
triggerLocation,
});
}, [track, currentSubscription]);
/**
* 追踪付费墙关闭
* @param {string} feature - 功能名称
* @param {string} closeMethod - 关闭方式 ('dismiss' | 'upgrade_clicked' | 'back_button')
*/
const trackPaywallDismissed = useCallback((feature, closeMethod = 'dismiss') => {
if (!feature) {
logger.warn('useSubscriptionEvents', 'trackPaywallDismissed: feature is required');
return;
}
track(REVENUE_EVENTS.PAYWALL_DISMISSED, {
feature,
close_method: closeMethod,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '❌ Paywall Dismissed', {
feature,
closeMethod,
});
}, [track, currentSubscription]);
/**
* 追踪升级按钮点击
* @param {string} targetPlan - 目标订阅计划
* @param {string} source - 来源位置
* @param {string} feature - 关联的功能(如果从付费墙点击)
*/
const trackUpgradePlanClicked = useCallback((targetPlan = 'pro', source = '', feature = '') => {
track(REVENUE_EVENTS.PAYWALL_UPGRADE_CLICKED, {
current_plan: currentSubscription?.plan || 'free',
target_plan: targetPlan,
source,
feature: feature || null,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '⬆️ Upgrade Plan Clicked', {
currentPlan: currentSubscription?.plan,
targetPlan,
source,
feature,
});
}, [track, currentSubscription]);
/**
* 追踪订阅页面查看
* @param {string} source - 来源
*/
const trackSubscriptionPageViewed = useCallback((source = '') => {
track(RETENTION_EVENTS.SUBSCRIPTION_PAGE_VIEWED, {
current_plan: currentSubscription?.plan || 'free',
subscription_status: currentSubscription?.status || 'unknown',
is_paid_user: currentSubscription?.plan && currentSubscription.plan !== 'free',
source,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '💳 Subscription Page Viewed', {
currentPlan: currentSubscription?.plan,
source,
});
}, [track, currentSubscription]);
/**
* 追踪定价计划查看
* @param {string} planName - 计划名称 ('free' | 'pro' | 'enterprise')
* @param {number} price - 价格
*/
const trackPricingPlanViewed = useCallback((planName, price = 0) => {
if (!planName) {
logger.warn('useSubscriptionEvents', 'trackPricingPlanViewed: planName is required');
return;
}
track('Pricing Plan Viewed', {
plan_name: planName,
price,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '👀 Pricing Plan Viewed', {
planName,
price,
});
}, [track, currentSubscription]);
/**
* 追踪定价计划选择
* @param {string} planName - 选择的计划名称
* @param {string} billingCycle - 计费周期 ('monthly' | 'yearly')
* @param {number} price - 价格
*/
const trackPricingPlanSelected = useCallback((planName, billingCycle = 'monthly', price = 0) => {
if (!planName) {
logger.warn('useSubscriptionEvents', 'trackPricingPlanSelected: planName is required');
return;
}
track('Pricing Plan Selected', {
plan_name: planName,
billing_cycle: billingCycle,
price,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '✅ Pricing Plan Selected', {
planName,
billingCycle,
price,
});
}, [track, currentSubscription]);
/**
* 追踪支付页面查看
* @param {string} planName - 购买的计划
* @param {number} amount - 支付金额
*/
const trackPaymentPageViewed = useCallback((planName, amount = 0) => {
track(REVENUE_EVENTS.PAYMENT_PAGE_VIEWED, {
plan_name: planName,
amount,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '💰 Payment Page Viewed', {
planName,
amount,
});
}, [track, currentSubscription]);
/**
* 追踪支付方式选择
* @param {string} paymentMethod - 支付方式 ('wechat_pay' | 'alipay' | 'credit_card')
* @param {number} amount - 支付金额
*/
const trackPaymentMethodSelected = useCallback((paymentMethod, amount = 0) => {
if (!paymentMethod) {
logger.warn('useSubscriptionEvents', 'trackPaymentMethodSelected: paymentMethod is required');
return;
}
track(REVENUE_EVENTS.PAYMENT_METHOD_SELECTED, {
payment_method: paymentMethod,
amount,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '💳 Payment Method Selected', {
paymentMethod,
amount,
});
}, [track, currentSubscription]);
/**
* 追踪支付发起
* @param {Object} paymentInfo - 支付信息
* @param {string} paymentInfo.planName - 计划名称
* @param {string} paymentInfo.paymentMethod - 支付方式
* @param {number} paymentInfo.amount - 金额
* @param {string} paymentInfo.billingCycle - 计费周期
* @param {string} paymentInfo.orderId - 订单ID
*/
const trackPaymentInitiated = useCallback((paymentInfo = {}) => {
track(REVENUE_EVENTS.PAYMENT_INITIATED, {
plan_name: paymentInfo.planName,
payment_method: paymentInfo.paymentMethod,
amount: paymentInfo.amount,
billing_cycle: paymentInfo.billingCycle,
order_id: paymentInfo.orderId,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🚀 Payment Initiated', {
planName: paymentInfo.planName,
amount: paymentInfo.amount,
paymentMethod: paymentInfo.paymentMethod,
});
}, [track, currentSubscription]);
/**
* 追踪支付成功
* @param {Object} paymentInfo - 支付信息
*/
const trackPaymentSuccessful = useCallback((paymentInfo = {}) => {
track(REVENUE_EVENTS.PAYMENT_SUCCESSFUL, {
plan_name: paymentInfo.planName,
payment_method: paymentInfo.paymentMethod,
amount: paymentInfo.amount,
billing_cycle: paymentInfo.billingCycle,
order_id: paymentInfo.orderId,
transaction_id: paymentInfo.transactionId,
previous_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '✅ Payment Successful', {
planName: paymentInfo.planName,
amount: paymentInfo.amount,
orderId: paymentInfo.orderId,
});
}, [track, currentSubscription]);
/**
* 追踪支付失败
* @param {Object} paymentInfo - 支付信息
* @param {string} errorReason - 失败原因
*/
const trackPaymentFailed = useCallback((paymentInfo = {}, errorReason = '') => {
track(REVENUE_EVENTS.PAYMENT_FAILED, {
plan_name: paymentInfo.planName,
payment_method: paymentInfo.paymentMethod,
amount: paymentInfo.amount,
error_reason: errorReason,
order_id: paymentInfo.orderId,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '❌ Payment Failed', {
planName: paymentInfo.planName,
errorReason,
orderId: paymentInfo.orderId,
});
}, [track, currentSubscription]);
/**
* 追踪订阅创建成功
* @param {Object} subscription - 订阅信息
*/
const trackSubscriptionCreated = useCallback((subscription = {}) => {
track(REVENUE_EVENTS.SUBSCRIPTION_CREATED, {
plan_name: subscription.plan,
billing_cycle: subscription.billingCycle,
amount: subscription.amount,
start_date: subscription.startDate,
end_date: subscription.endDate,
previous_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🎉 Subscription Created', {
plan: subscription.plan,
billingCycle: subscription.billingCycle,
});
}, [track, currentSubscription]);
/**
* 追踪订阅续费
* @param {Object} subscription - 订阅信息
*/
const trackSubscriptionRenewed = useCallback((subscription = {}) => {
track(REVENUE_EVENTS.SUBSCRIPTION_RENEWED, {
plan_name: subscription.plan,
amount: subscription.amount,
previous_end_date: subscription.previousEndDate,
new_end_date: subscription.newEndDate,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🔄 Subscription Renewed', {
plan: subscription.plan,
amount: subscription.amount,
});
}, [track]);
/**
* 追踪订阅取消
* @param {string} reason - 取消原因
* @param {boolean} cancelImmediately - 是否立即取消
*/
const trackSubscriptionCancelled = useCallback((reason = '', cancelImmediately = false) => {
track(REVENUE_EVENTS.SUBSCRIPTION_CANCELLED, {
plan_name: currentSubscription?.plan,
reason,
has_reason: Boolean(reason),
cancel_immediately: cancelImmediately,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🚫 Subscription Cancelled', {
plan: currentSubscription?.plan,
reason,
cancelImmediately,
});
}, [track, currentSubscription]);
/**
* 追踪优惠券应用
* @param {string} couponCode - 优惠券代码
* @param {number} discountAmount - 折扣金额
* @param {boolean} success - 是否成功
*/
const trackCouponApplied = useCallback((couponCode, discountAmount = 0, success = true) => {
if (!couponCode) {
logger.warn('useSubscriptionEvents', 'trackCouponApplied: couponCode is required');
return;
}
track('Coupon Applied', {
coupon_code: couponCode,
discount_amount: discountAmount,
success,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', success ? '🎟️ Coupon Applied' : '❌ Coupon Failed', {
couponCode,
discountAmount,
success,
});
}, [track, currentSubscription]);
return {
// 付费墙事件
trackPaywallShown,
trackPaywallDismissed,
trackUpgradePlanClicked,
// 订阅页面事件
trackSubscriptionPageViewed,
trackPricingPlanViewed,
trackPricingPlanSelected,
// 支付流程事件
trackPaymentPageViewed,
trackPaymentMethodSelected,
trackPaymentInitiated,
trackPaymentSuccessful,
trackPaymentFailed,
// 订阅管理事件
trackSubscriptionCreated,
trackSubscriptionRenewed,
trackSubscriptionCancelled,
// 优惠券事件
trackCouponApplied,
};
};
export default useSubscriptionEvents;

View File

@@ -0,0 +1,382 @@
// src/hooks/useSubscriptionEvents.ts
// 订阅和支付事件追踪 Hook
import { useCallback } from 'react';
import { usePostHogTrack } from './usePostHogRedux';
import { RETENTION_EVENTS, REVENUE_EVENTS } from '../lib/constants';
/**
* 当前订阅信息
*/
interface SubscriptionInfo {
plan?: string;
status?: string;
}
/**
* useSubscriptionEvents Hook 配置选项
*/
interface UseSubscriptionEventsOptions {
currentSubscription?: SubscriptionInfo | null;
}
/**
* 支付信息
*/
interface PaymentInfo {
planName?: string;
paymentMethod?: string;
amount?: number;
billingCycle?: string;
orderId?: string;
transactionId?: string;
}
/**
* 订阅信息
*/
interface SubscriptionData {
plan?: string;
billingCycle?: string;
amount?: number;
startDate?: string;
endDate?: string;
previousEndDate?: string;
newEndDate?: string;
}
/**
* useSubscriptionEvents Hook 返回值
*/
interface UseSubscriptionEventsReturn {
trackPaywallShown: (feature: string, requiredPlan?: string, triggerLocation?: string) => void;
trackPaywallDismissed: (feature: string, closeMethod?: string) => void;
trackUpgradePlanClicked: (targetPlan?: string, source?: string, feature?: string) => void;
trackSubscriptionPageViewed: (source?: string) => void;
trackPricingPlanViewed: (planName: string, price?: number) => void;
trackPricingPlanSelected: (planName: string, billingCycle?: string, price?: number) => void;
trackPaymentPageViewed: (planName: string, amount?: number) => void;
trackPaymentMethodSelected: (paymentMethod: string, amount?: number) => void;
trackPaymentInitiated: (paymentInfo?: PaymentInfo) => void;
trackPaymentSuccessful: (paymentInfo?: PaymentInfo) => void;
trackPaymentFailed: (paymentInfo?: PaymentInfo, errorReason?: string) => void;
trackSubscriptionCreated: (subscription?: SubscriptionData) => void;
trackSubscriptionRenewed: (subscription?: SubscriptionData) => void;
trackSubscriptionCancelled: (reason?: string, cancelImmediately?: boolean) => void;
trackCouponApplied: (couponCode: string, discountAmount?: number, success?: boolean) => void;
}
/**
* 订阅和支付事件追踪 Hook
* @param options - 配置选项
* @returns 事件追踪处理函数集合
*/
export const useSubscriptionEvents = ({
currentSubscription = null,
}: UseSubscriptionEventsOptions = {}): UseSubscriptionEventsReturn => {
const { track } = usePostHogTrack();
/**
* 追踪付费墙展示
*/
const trackPaywallShown = useCallback(
(feature: string, requiredPlan: string = 'pro', triggerLocation: string = '') => {
if (!feature) {
console.warn('useSubscriptionEvents: trackPaywallShown - feature is required');
return;
}
track(REVENUE_EVENTS.PAYWALL_SHOWN, {
feature,
required_plan: requiredPlan,
current_plan: currentSubscription?.plan || 'free',
trigger_location: triggerLocation,
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪付费墙关闭
*/
const trackPaywallDismissed = useCallback(
(feature: string, closeMethod: string = 'dismiss') => {
if (!feature) {
console.warn('useSubscriptionEvents: trackPaywallDismissed - feature is required');
return;
}
track(REVENUE_EVENTS.PAYWALL_DISMISSED, {
feature,
close_method: closeMethod,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪升级按钮点击
*/
const trackUpgradePlanClicked = useCallback(
(targetPlan: string = 'pro', source: string = '', feature: string = '') => {
track(REVENUE_EVENTS.PAYWALL_UPGRADE_CLICKED, {
current_plan: currentSubscription?.plan || 'free',
target_plan: targetPlan,
source,
feature: feature || null,
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪订阅页面查看
*/
const trackSubscriptionPageViewed = useCallback(
(source: string = '') => {
track(RETENTION_EVENTS.SUBSCRIPTION_PAGE_VIEWED, {
current_plan: currentSubscription?.plan || 'free',
subscription_status: currentSubscription?.status || 'unknown',
is_paid_user: currentSubscription?.plan && currentSubscription.plan !== 'free',
source,
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪定价计划查看
*/
const trackPricingPlanViewed = useCallback(
(planName: string, price: number = 0) => {
if (!planName) {
console.warn('useSubscriptionEvents: trackPricingPlanViewed - planName is required');
return;
}
track('Pricing Plan Viewed', {
plan_name: planName,
price,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪定价计划选择
*/
const trackPricingPlanSelected = useCallback(
(planName: string, billingCycle: string = 'monthly', price: number = 0) => {
if (!planName) {
console.warn('useSubscriptionEvents: trackPricingPlanSelected - planName is required');
return;
}
track('Pricing Plan Selected', {
plan_name: planName,
billing_cycle: billingCycle,
price,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪支付页面查看
*/
const trackPaymentPageViewed = useCallback(
(planName: string, amount: number = 0) => {
track(REVENUE_EVENTS.PAYMENT_PAGE_VIEWED, {
plan_name: planName,
amount,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪支付方式选择
*/
const trackPaymentMethodSelected = useCallback(
(paymentMethod: string, amount: number = 0) => {
if (!paymentMethod) {
console.warn('useSubscriptionEvents: trackPaymentMethodSelected - paymentMethod is required');
return;
}
track(REVENUE_EVENTS.PAYMENT_METHOD_SELECTED, {
payment_method: paymentMethod,
amount,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪支付发起
*/
const trackPaymentInitiated = useCallback(
(paymentInfo: PaymentInfo = {}) => {
track(REVENUE_EVENTS.PAYMENT_INITIATED, {
plan_name: paymentInfo.planName,
payment_method: paymentInfo.paymentMethod,
amount: paymentInfo.amount,
billing_cycle: paymentInfo.billingCycle,
order_id: paymentInfo.orderId,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪支付成功
*/
const trackPaymentSuccessful = useCallback(
(paymentInfo: PaymentInfo = {}) => {
track(REVENUE_EVENTS.PAYMENT_SUCCESSFUL, {
plan_name: paymentInfo.planName,
payment_method: paymentInfo.paymentMethod,
amount: paymentInfo.amount,
billing_cycle: paymentInfo.billingCycle,
order_id: paymentInfo.orderId,
transaction_id: paymentInfo.transactionId,
previous_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪支付失败
*/
const trackPaymentFailed = useCallback(
(paymentInfo: PaymentInfo = {}, errorReason: string = '') => {
track(REVENUE_EVENTS.PAYMENT_FAILED, {
plan_name: paymentInfo.planName,
payment_method: paymentInfo.paymentMethod,
amount: paymentInfo.amount,
error_reason: errorReason,
order_id: paymentInfo.orderId,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪订阅创建成功
*/
const trackSubscriptionCreated = useCallback(
(subscription: SubscriptionData = {}) => {
track(REVENUE_EVENTS.SUBSCRIPTION_CREATED, {
plan_name: subscription.plan,
billing_cycle: subscription.billingCycle,
amount: subscription.amount,
start_date: subscription.startDate,
end_date: subscription.endDate,
previous_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪订阅续费
*/
const trackSubscriptionRenewed = useCallback(
(subscription: SubscriptionData = {}) => {
track(REVENUE_EVENTS.SUBSCRIPTION_RENEWED, {
plan_name: subscription.plan,
amount: subscription.amount,
previous_end_date: subscription.previousEndDate,
new_end_date: subscription.newEndDate,
timestamp: new Date().toISOString(),
});
},
[track]
);
/**
* 追踪订阅取消
*/
const trackSubscriptionCancelled = useCallback(
(reason: string = '', cancelImmediately: boolean = false) => {
track(REVENUE_EVENTS.SUBSCRIPTION_CANCELLED, {
plan_name: currentSubscription?.plan,
reason,
has_reason: Boolean(reason),
cancel_immediately: cancelImmediately,
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
/**
* 追踪优惠券应用
*/
const trackCouponApplied = useCallback(
(couponCode: string, discountAmount: number = 0, success: boolean = true) => {
if (!couponCode) {
console.warn('useSubscriptionEvents: trackCouponApplied - couponCode is required');
return;
}
track('Coupon Applied', {
coupon_code: couponCode,
discount_amount: discountAmount,
success,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
},
[track, currentSubscription]
);
return {
// 付费墙事件
trackPaywallShown,
trackPaywallDismissed,
trackUpgradePlanClicked,
// 订阅页面事件
trackSubscriptionPageViewed,
trackPricingPlanViewed,
trackPricingPlanSelected,
// 支付流程事件
trackPaymentPageViewed,
trackPaymentMethodSelected,
trackPaymentInitiated,
trackPaymentSuccessful,
trackPaymentFailed,
// 订阅管理事件
trackSubscriptionCreated,
trackSubscriptionRenewed,
trackSubscriptionCancelled,
// 优惠券事件
trackCouponApplied,
};
};
export default useSubscriptionEvents;

View File

@@ -3,8 +3,12 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
// 导入 Brainwave 样式(空文件,保留以避免错误)
import './styles/brainwave.css';
// ⚡ 性能监控:在应用启动时尽早标记
import { performanceMonitor } from './utils/performanceMonitor';
performanceMonitor.mark('app-start');
// ⚡ 已删除 brainwave.css项目未安装 Tailwind CSS该文件无效
// import './styles/brainwave.css';
// 导入 Select 下拉框颜色修复样式
import './styles/select-fix.css';
@@ -36,98 +40,26 @@ if (process.env.REACT_APP_ENABLE_DEBUG === 'true') {
function registerServiceWorker() {
// ⚠️ Mock 模式下跳过 Service Worker 注册(避免与 MSW 冲突)
if (process.env.REACT_APP_ENABLE_MOCK === 'true') {
console.log(
'%c[App] Mock 模式已启用,跳过通知 Service Worker 注册(避免与 MSW 冲突)',
'color: #FF9800; font-weight: bold;'
);
return;
}
// 仅在支持 Service Worker 的浏览器中注册
if ('serviceWorker' in navigator) {
// 在页面加载完成后注册
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then((registration) => {
console.log('[App] ✅ Service Worker 注册成功');
console.log('[App] Scope:', registration.scope);
// 检查当前激活状态
if (navigator.serviceWorker.controller) {
console.log('[App] ✅ Service Worker 已激活并控制页面');
} else {
console.log('[App] ⏳ Service Worker 已注册,等待激活...');
console.log('[App] 💡 刷新页面以激活 Service Worker');
// 监听 controller 变化Service Worker 激活后触发)
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('[App] ✅ Service Worker 控制器已更新');
});
}
// 监听 Service Worker 更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('[App] 🔄 发现 Service Worker 更新');
if (newWorker) {
newWorker.addEventListener('statechange', () => {
console.log(`[App] Service Worker 状态: ${newWorker.state}`);
if (newWorker.state === 'activated') {
console.log('[App] ✅ Service Worker 已激活');
// 如果有旧的 Service Worker 在控制页面,提示用户刷新
if (navigator.serviceWorker.controller) {
console.log('[App] 💡 Service Worker 已更新,建议刷新页面');
}
}
});
}
});
})
.catch((error) => {
console.error('[App] Service Worker 注册失败');
console.error('[App] 错误类型:', error.name);
console.error('[App] 错误信息:', error.message);
console.error('[App] 完整错误:', error);
// 额外检查:验证文件是否可访问
fetch('/service-worker.js', { method: 'HEAD' })
.then(response => {
if (response.ok) {
console.error('[App] Service Worker 文件存在但注册失败');
console.error('[App] 💡 可能的原因:');
console.error('[App] 1. Service Worker 文件有语法错误');
console.error('[App] 2. 浏览器不支持某些 Service Worker 特性');
console.error('[App] 3. HTTPS 证书问题Service Worker 需要 HTTPS');
} else {
console.error('[App] Service Worker 文件不存在HTTP', response.status, '');
}
})
.catch(fetchError => {
console.error('[App] 无法访问 Service Worker 文件:', fetchError.message);
});
console.error('[App] Service Worker 注册失败:', error.message);
});
});
} else {
console.warn('[App] Service Worker is not supported in this browser');
}
}
// 启动 Mock Service Worker如果启用
async function startApp() {
// 只在开发环境启动 MSW
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_ENABLE_MOCK === 'true') {
const { startMockServiceWorker } = await import('./mocks/browser');
await startMockServiceWorker();
}
// Create root
// 渲染应用
function renderApp() {
const root = ReactDOM.createRoot(document.getElementById('root'));
// Render the app with Router wrapper
// ✅ StrictMode 已启用Chakra UI 2.10.9+ 已修复兼容性问题)
// StrictMode 已启用Chakra UI 2.10.9+ 已修复兼容性问题)
root.render(
<React.StrictMode>
<Router
@@ -141,9 +73,26 @@ async function startApp() {
</React.StrictMode>
);
// 注册 Service Worker
// 注册 Service Worker(非 Mock 模式)
registerServiceWorker();
}
// 启动应用
async function startApp() {
// ✅ 开发环境 Mock 模式:先启动 MSW再渲染应用
// 确保所有 API 请求(包括 AuthContext.checkSession都被正确拦截
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_ENABLE_MOCK === 'true') {
try {
const { startMockServiceWorker } = await import('./mocks/browser');
await startMockServiceWorker();
} catch (error) {
console.error('[MSW] 启动失败:', error);
}
}
// 渲染应用
renderApp();
}
// 启动应用
startApp();

View File

@@ -35,7 +35,7 @@ export default function MainLayout() {
<MemoizedHomeNavbar />
{/* 页面内容区域 - flex: 1 占据剩余空间,包含错误边界、懒加载 */}
<Box flex="1" pt="72px">
<Box flex="1" pt="60px">
<ErrorBoundary>
<Suspense fallback={<PageLoader message="页面加载中..." />}>
<Outlet />

View File

@@ -47,18 +47,8 @@ export async function startMockServiceWorker() {
});
isStarted = true;
console.log(
'%c[MSW] Mock Service Worker 已启动 🎭 (警告模式)',
'color: #4CAF50; font-weight: bold; font-size: 14px;'
);
console.log(
'%c警告模式已定义 Mock → 返回假数据 | 未定义 Mock → 显示警告 ⚠️ | 允许 passthrough',
'color: #FF9800; font-weight: bold; font-size: 12px;'
);
console.log(
'%c查看 src/mocks/handlers/ 目录管理 Mock 接口',
'color: #2196F3; font-size: 12px;'
);
// 精简日志:只保留一行启动提示
console.log('%c[MSW] Mock 已启用 🎭', 'color: #4CAF50; font-weight: bold;');
} catch (error) {
console.error('[MSW] 启动失败:', error);
} finally {

View File

@@ -102,7 +102,6 @@ export function setCurrentUser(user) {
subscription_days_left: user.subscription_days_left || 0
};
localStorage.setItem('mock_current_user', JSON.stringify(normalizedUser));
console.log('[Mock State] 设置当前登录用户:', normalizedUser);
}
}

View File

@@ -613,14 +613,6 @@ export const accountHandlers = [
end_date: currentUser.subscription_end_date || null
};
console.log('[Mock API] 获取当前订阅详情:', {
user_id: currentUser.id,
phone: currentUser.phone,
subscription_type: userSubscriptionType,
subscription_status: subscriptionDetails.status,
days_left: subscriptionDetails.days_left
});
return HttpResponse.json({
success: true,
data: subscriptionDetails

View File

@@ -12,7 +12,8 @@ import {
} from '../data/users';
// 模拟网络延迟(毫秒)
const NETWORK_DELAY = 500;
// ⚡ 开发环境使用较短延迟,加快首屏加载速度
const NETWORK_DELAY = 50;
export const authHandlers = [
// ==================== 手机验证码登录 ====================
@@ -31,21 +32,6 @@ export const authHandlers = [
expiresAt: Date.now() + 5 * 60 * 1000 // 5分钟后过期
});
// 超醒目的验证码提示 - 方便开发调试
console.log(
`%c\n` +
`╔════════════════════════════════════════════╗\n` +
`║ 验证码: ${code.padEnd(22)}\n` +
`╚════════════════════════════════════════════╝\n`,
'color: #ffffff; background: #16a34a; font-weight: bold; font-size: 16px; padding: 20px; line-height: 1.8;'
);
// 额外的高亮提示
console.log(
`%c 验证码: ${code} `,
'color: #ffffff; background: #dc2626; font-weight: bold; font-size: 24px; padding: 15px 30px; border-radius: 8px; margin: 10px 0;'
);
return HttpResponse.json({
success: true,
message: `验证码已发送到 ${credential}Mock: ${code}`,
@@ -141,8 +127,6 @@ export const authHandlers = [
const body = await request.json();
const { credential, verification_code, login_type } = body;
console.log('[Mock] 验证码登录:', { credential, verification_code, login_type });
// 验证验证码
const storedCode = mockVerificationCodes.get(credential);
if (!storedCode) {
@@ -194,11 +178,8 @@ export const authHandlers = [
subscription_days_left: 0
};
mockUsers[credential] = user;
console.log('[Mock] 创建新用户:', user);
}
console.log('[Mock] 登录成功:', user);
// 设置当前登录用户
setCurrentUser(user);
@@ -345,25 +326,22 @@ export const authHandlers = [
});
}),
// 6. 获取微信 H5 授权 URL
http.post('/api/auth/wechat/h5-auth-url', async ({ request }) => {
// 6. 获取微信 H5 授权 URL(手机浏览器用)
http.post('/api/auth/wechat/h5-auth', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { redirect_url } = body;
const state = generateWechatSessionId();
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=mock&redirect_uri=${encodeURIComponent(redirect_url)}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
// Mock 模式下直接返回前端回调地址(模拟授权成功)
const authUrl = `${redirect_url}?wechat_login=success&state=${state}`;
console.log('[Mock] 生成微信 H5 授权 URL:', authUrl);
return HttpResponse.json({
code: 0,
message: '成功',
data: {
auth_url: authUrl,
state
}
auth_url: authUrl,
state
});
}),
@@ -371,13 +349,11 @@ export const authHandlers = [
// 7. 检查 SessionAuthContext 使用的正确端点)
http.get('/api/auth/session', async () => {
await delay(300);
await delay(NETWORK_DELAY); // ⚡ 使用统一延迟配置
// 获取当前登录用户
const currentUser = getCurrentUser();
console.log('[Mock] 检查 Session:', currentUser);
if (currentUser) {
return HttpResponse.json({
success: true,
@@ -395,13 +371,11 @@ export const authHandlers = [
// 8. 检查 Session旧端点保留兼容
http.get('/api/auth/check-session', async () => {
await delay(300);
await delay(NETWORK_DELAY); // ⚡ 使用统一延迟配置
// 获取当前登录用户
const currentUser = getCurrentUser();
console.log('[Mock] 检查 Session (旧端点):', currentUser);
if (currentUser) {
return HttpResponse.json({
success: true,
@@ -432,91 +406,3 @@ export const authHandlers = [
});
})
];
// ==================== Mock 调试工具(仅开发环境) ====================
/**
* 暴露全局API方便手动触发微信扫码模拟
* 使用方式:
* 1. 浏览器控制台输入window.mockWechatScan()
* 2. 或者在组件中调用window.mockWechatScan(sessionId)
*/
if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_MOCK === 'true') {
window.mockWechatScan = (sessionId) => {
// 如果没有传入sessionId尝试获取最新的session
let targetSessionId = sessionId;
if (!targetSessionId) {
// 获取最新创建的session
const sessions = Array.from(mockWechatSessions.entries());
if (sessions.length === 0) {
console.warn('[Mock API] 没有活跃的微信session请先获取二维码');
return false;
}
// 按创建时间排序,获取最新的
const latestSession = sessions.sort((a, b) => b[1].createdAt - a[1].createdAt)[0];
targetSessionId = latestSession[0];
}
const session = mockWechatSessions.get(targetSessionId);
if (!session) {
console.error('[Mock API] Session不存在:', targetSessionId);
return false;
}
if (session.status !== 'waiting') {
console.warn('[Mock API] Session状态不是waiting当前状态:', session.status);
return false;
}
// 立即触发扫码
session.status = 'scanned';
console.log(`[Mock API] ✅ 模拟扫码成功: ${targetSessionId}`);
// 1秒后自动确认登录
setTimeout(() => {
const session2 = mockWechatSessions.get(targetSessionId);
if (session2 && session2.status === 'scanned') {
session2.status = 'authorized'; // ✅ 使用 'authorized' 状态,与自动扫码流程保持一致
session2.user = {
id: 999,
nickname: '微信测试用户',
wechat_openid: 'mock_openid_' + targetSessionId,
avatar_url: 'https://ui-avatars.com/api/?name=微信测试用户&size=150&background=4299e1&color=fff',
phone: null,
email: null,
has_wechat: true,
created_at: new Date().toISOString(),
subscription_type: 'free',
subscription_status: 'active',
subscription_end_date: null,
is_subscription_active: true,
subscription_days_left: 0
};
session2.user_info = { user_id: session2.user.id }; // ✅ 添加 user_info 字段
console.log(`[Mock API] ✅ 模拟确认登录: ${targetSessionId}`, session2.user);
}
}, 1000);
return true;
};
// 暴露获取当前sessions的方法调试用
window.getMockWechatSessions = () => {
const sessions = Array.from(mockWechatSessions.entries()).map(([id, session]) => ({
sessionId: id,
status: session.status,
createdAt: new Date(session.createdAt).toLocaleString(),
hasUser: !!session.user
}));
console.table(sessions);
return sessions;
};
console.log('%c[Mock API] 微信登录调试工具已加载', 'color: #00D084; font-weight: bold');
console.log('%c使用方法:', 'color: #666');
console.log(' window.mockWechatScan() - 触发最新session的扫码');
console.log(' window.mockWechatScan(sessionId) - 触发指定session的扫码');
console.log(' window.getMockWechatSessions() - 查看所有活跃的sessions');
}

View File

@@ -0,0 +1,22 @@
// src/mocks/handlers/bytedesk.js
/**
* Bytedesk 客服 Widget MSW Handler
* 使用 passthrough 让请求通过到真实服务器,消除 MSW 警告
*/
import { http, passthrough } from 'msw';
export const bytedeskHandlers = [
// Bytedesk API 请求 - 直接 passthrough
// 匹配 /bytedesk/* 路径(通过代理访问后端)
http.all('/bytedesk/*', () => {
return passthrough();
}),
// Bytedesk 外部 CDN/服务请求
http.all('https://www.weiyuai.cn/*', () => {
return passthrough();
}),
];
export default bytedeskHandlers;

View File

@@ -16,6 +16,7 @@ import { limitAnalyseHandlers } from './limitAnalyse';
import { posthogHandlers } from './posthog';
import { externalHandlers } from './external';
import { agentHandlers } from './agent';
import { bytedeskHandlers } from './bytedesk';
// 可以在这里添加更多的 handlers
// import { userHandlers } from './user';
@@ -36,5 +37,6 @@ export const handlers = [
...posthogHandlers,
...externalHandlers,
...agentHandlers,
...bytedeskHandlers, // ⚡ Bytedesk 客服 Widget passthrough
// ...userHandlers,
];

View File

@@ -188,46 +188,3 @@ export const paymentHandlers = [
});
})
];
// ==================== Mock 调试工具(仅开发环境) ====================
/**
* 暴露全局API方便手动触发支付成功
* 使用方式window.mockPaymentSuccess(orderId)
*/
if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_MOCK === 'true') {
window.mockPaymentSuccess = (orderId) => {
const order = mockOrders.get(orderId);
if (!order) {
console.error('[Mock Payment] 订单不存在:', orderId);
return false;
}
if (order.status !== 'pending') {
console.warn('[Mock Payment] 订单状态不是待支付:', order.status);
return false;
}
order.status = 'paid';
order.paid_at = new Date().toISOString();
console.log('[Mock Payment] ✅ 支付成功:', orderId);
return true;
};
window.getMockOrders = () => {
const orders = Array.from(mockOrders.entries()).map(([id, order]) => ({
orderId: id,
status: order.status,
amount: order.amount,
plan: `${order.plan_name} - ${order.billing_cycle}`,
createdAt: new Date(order.created_at).toLocaleString()
}));
console.table(orders);
return orders;
};
console.log('%c[Mock Payment] 支付调试工具已加载', 'color: #00D084; font-weight: bold');
console.log('%c使用方法:', 'color: #666');
console.log(' window.mockPaymentSuccess(orderId) - 手动触发订单支付成功');
console.log(' window.getMockOrders() - 查看所有模拟订单');
}

View File

@@ -9,43 +9,44 @@ import React from 'react';
*/
export const lazyComponents = {
// Home 模块
HomePage: React.lazy(() => import('../views/Home/HomePage')),
CenterDashboard: React.lazy(() => import('../views/Dashboard/Center')),
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')),
UserAgreement: React.lazy(() => import('../views/Pages/UserAgreement')),
WechatCallback: React.lazy(() => import('../views/Pages/WechatCallback')),
// ⚡ 直接引用 HomePage无需中间层静态页面不需要骨架屏
HomePage: React.lazy(() => import('@views/Home/HomePage')),
CenterDashboard: React.lazy(() => import('@views/Dashboard/Center')),
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')),
UserAgreement: React.lazy(() => import('@views/Pages/UserAgreement')),
WechatCallback: React.lazy(() => import('@views/Pages/WechatCallback')),
// 社区/内容模块
Community: React.lazy(() => import('../views/Community')),
ConceptCenter: React.lazy(() => import('../views/Concept')),
StockOverview: React.lazy(() => import('../views/StockOverview')),
LimitAnalyse: React.lazy(() => import('../views/LimitAnalyse')),
Community: React.lazy(() => import('@views/Community')),
ConceptCenter: React.lazy(() => import('@views/Concept')),
StockOverview: React.lazy(() => import('@views/StockOverview')),
LimitAnalyse: React.lazy(() => import('@views/LimitAnalyse')),
// 交易模块
TradingSimulation: React.lazy(() => import('../views/TradingSimulation')),
TradingSimulation: React.lazy(() => import('@views/TradingSimulation')),
// 事件模块
EventDetail: React.lazy(() => import('../views/EventDetail')),
EventDetail: React.lazy(() => import('@views/EventDetail')),
// 公司相关模块
CompanyIndex: React.lazy(() => import('../views/Company')),
ForecastReport: React.lazy(() => import('../views/Company/ForecastReport')),
FinancialPanorama: React.lazy(() => import('../views/Company/FinancialPanorama')),
MarketDataView: React.lazy(() => import('../views/Company/MarketDataView')),
CompanyIndex: React.lazy(() => import('@views/Company')),
ForecastReport: React.lazy(() => import('@views/Company/ForecastReport')),
FinancialPanorama: React.lazy(() => import('@views/Company/FinancialPanorama')),
MarketDataView: React.lazy(() => import('@views/Company/MarketDataView')),
// Agent模块
AgentChat: React.lazy(() => import('../views/AgentChat')),
AgentChat: React.lazy(() => import('@views/AgentChat')),
// 价值论坛模块
ValueForum: React.lazy(() => import('../views/ValueForum')),
ForumPostDetail: React.lazy(() => import('../views/ValueForum/PostDetail')),
PredictionTopicDetail: React.lazy(() => import('../views/ValueForum/PredictionTopicDetail')),
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')),
DataBrowser: React.lazy(() => import('@views/DataBrowser')),
};
/**

View File

@@ -10,25 +10,12 @@ import { socketService } from '../socketService';
export const socket = socketService;
export { socketService };
// ⚡ 新增:暴露 Socket 实例到 window用于调试和验证
// ⚡ 暴露 Socket 实例到 window用于调试和验证
// 注意:移除首屏加载时的日志,避免阻塞感知
if (typeof window !== 'undefined') {
window.socket = socketService;
window.socketService = socketService;
console.log(
'%c[Socket Service] ✅ Socket instance exposed to window',
'color: #4CAF50; font-weight: bold; font-size: 14px;'
);
console.log(' 📍 window.socket:', window.socket);
console.log(' 📍 window.socketService:', window.socketService);
console.log(' 📍 Socket.IO instance:', window.socket?.socket);
console.log(' 📍 Connection status:', window.socket?.connected ? '✅ Connected' : '❌ Disconnected');
// 日志已移除,如需调试可在控制台执行: console.log(window.socket)
}
// 打印当前使用的服务类型
console.log(
'%c[Socket Service] Using REAL Socket Service',
'color: #4CAF50; font-weight: bold; font-size: 12px;'
);
export default socket;

View File

@@ -56,17 +56,8 @@ class SocketService {
// 注册所有暂存的事件监听器(保留 pendingListeners不清空
if (this.pendingListeners.length > 0) {
console.log(`[socketService] 📦 注册 ${this.pendingListeners.length} 个暂存的事件监听器`);
this.pendingListeners.forEach(({ event, callback }) => {
// 直接在 Socket.IO 实例上注册(避免递归调用 this.on()
const wrappedCallback = (...args) => {
console.log(`%c[socketService] 🔔 收到原始事件: ${event}`, 'color: #2196F3; font-weight: bold;');
console.log(`[socketService] 事件数据 (${event}):`, ...args);
callback(...args);
};
this.socket.on(event, wrappedCallback);
console.log(`[socketService] ✓ 已注册事件监听器: ${event}`);
this.socket.on(event, callback);
});
// ⚠️ 重要:不清空 pendingListeners保留用于重连
}
@@ -82,15 +73,8 @@ class SocketService {
this.customReconnectTimer = null;
}
logger.info('socketService', 'Socket.IO connected successfully', {
socketId: this.socket.id,
});
console.log(`%c[socketService] ✅ WebSocket 已连接`, 'color: #4CAF50; font-weight: bold;');
console.log('[socketService] Socket ID:', this.socket.id);
logger.info('socketService', 'Socket.IO connected', { socketId: this.socket.id });
// ⚠️ 已移除自动订阅,让 NotificationContext 负责订阅
// this.subscribeToAllEvents();
});
// 监听断开连接
@@ -174,25 +158,12 @@ class SocketService {
);
if (!exists) {
logger.info('socketService', 'Socket not ready, queuing listener', { event });
console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
this.pendingListeners.push({ event, callback });
} else {
console.log(`[socketService] ⚠️ 监听器已存在,跳过: ${event}`);
}
return;
}
// 包装回调函数,添加日志
const wrappedCallback = (...args) => {
console.log(`%c[socketService] 🔔 收到原始事件: ${event}`, 'color: #2196F3; font-weight: bold;');
console.log(`[socketService] 事件数据 (${event}):`, ...args);
callback(...args);
};
this.socket.on(event, wrappedCallback);
logger.info('socketService', `Event listener added: ${event}`);
console.log(`[socketService] ✓ 已注册事件监听器: ${event}`);
this.socket.on(event, callback);
}
/**
@@ -210,8 +181,6 @@ class SocketService {
} else {
this.socket.off(event);
}
logger.info('socketService', `Event listener removed: ${event}`);
}
/**
@@ -231,8 +200,6 @@ class SocketService {
} else {
this.socket.emit(event, data);
}
logger.info('socketService', `Event emitted: ${event}`, data);
}
/**
@@ -329,10 +296,18 @@ class SocketService {
onSubscribed,
} = options;
if (!this.socket || !this.connected) {
logger.warn('socketService', 'Cannot subscribe: socket not connected');
// 自动连接
this.connect();
// ⚡ 改进状态检查:同时检查 this.connected 和 socket.connected
// 解决 connect 回调中 this.connected 尚未更新的竞争条件
const isReady = this.socket && (this.socket.connected || this.connected);
if (!isReady) {
logger.debug('socketService', 'Socket 尚未就绪,等待连接后订阅');
if (!this.socket) {
// 自动连接
this.connect();
}
// 等待连接成功后再订阅
this.socket.once('connect', () => {
this._doSubscribe(eventType, importance, onNewEvent, onSubscribed);
@@ -347,65 +322,31 @@ class SocketService {
* 执行订阅操作(内部方法)
*/
_doSubscribe(eventType, importance, onNewEvent, onSubscribed) {
console.log('\n========== [SocketService DEBUG] 开始订阅 ==========');
console.log('[SocketService DEBUG] 事件类型:', eventType);
console.log('[SocketService DEBUG] 重要性:', importance);
console.log('[SocketService DEBUG] Socket 连接状态:', this.connected);
console.log('[SocketService DEBUG] Socket ID:', this.socket?.id);
// 发送订阅请求
const subscribeData = {
event_type: eventType,
importance: importance,
};
console.log('[SocketService DEBUG] 准备发送 subscribe_events:', subscribeData);
this.emit('subscribe_events', subscribeData);
console.log('[SocketService DEBUG] ✓ 已发送 subscribe_events');
// 监听订阅确认
this.socket.once('subscription_confirmed', (data) => {
console.log('\n[SocketService DEBUG] ========== 收到订阅确认 ==========');
console.log('[SocketService DEBUG] 订阅确认数据:', data);
logger.info('socketService', 'Subscription confirmed', data);
if (onSubscribed) {
console.log('[SocketService DEBUG] 调用 onSubscribed 回调');
onSubscribed(data);
}
console.log('[SocketService DEBUG] ========== 订阅确认处理完成 ==========\n');
});
// 监听订阅错误
this.socket.once('subscription_error', (error) => {
console.error('\n[SocketService ERROR] ========== 订阅错误 ==========');
console.error('[SocketService ERROR] 错误信息:', error);
logger.error('socketService', 'Subscription error', error);
console.error('[SocketService ERROR] ========== 订阅错误处理完成 ==========\n');
});
// 监听新事件推送
// ⚠️ 注意:不要移除其他地方注册的 new_event 监听器(如 NotificationContext
// 多个监听器可以共存,都会被触发
if (onNewEvent) {
console.log('[SocketService DEBUG] 设置 new_event 监听器');
// ⚠️ 已移除 this.socket.off('new_event'),允许多个监听器共存
// 添加新的监听器(与其他监听器共存)
this.socket.on('new_event', (eventData) => {
console.log('\n[SocketService DEBUG] ========== 收到新事件推送 ==========');
console.log('[SocketService DEBUG] 事件数据:', eventData);
console.log('[SocketService DEBUG] 事件 ID:', eventData?.id);
console.log('[SocketService DEBUG] 事件标题:', eventData?.title);
logger.info('socketService', 'New event received', eventData);
console.log('[SocketService DEBUG] 准备调用 onNewEvent 回调');
onNewEvent(eventData);
console.log('[SocketService DEBUG] ✓ onNewEvent 回调已调用');
console.log('[SocketService DEBUG] ========== 新事件处理完成 ==========\n');
});
console.log('[SocketService DEBUG] ✓ new_event 监听器已设置(与其他监听器共存)');
}
console.log('[SocketService DEBUG] ========== 订阅完成 ==========\n');
}
/**
@@ -432,11 +373,7 @@ class SocketService {
// 监听取消订阅确认
this.socket.once('unsubscription_confirmed', (data) => {
logger.info('socketService', 'Unsubscription confirmed', data);
// 移除新事件监听器
this.socket.off('new_event');
if (onUnsubscribed) {
onUnsubscribed(data);
}
@@ -454,22 +391,10 @@ class SocketService {
* @returns {Function} 取消订阅的函数
*/
subscribeToAllEvents(onNewEvent) {
console.log('%c[socketService] 🔔 自动订阅所有事件...', 'color: #FF9800; font-weight: bold;');
// 如果没有提供回调,添加一个默认的日志回调
const defaultCallback = (event) => {
console.log('%c[socketService] 📨 收到新事件(默认回调)', 'color: #4CAF50; font-weight: bold;');
console.log('[socketService] 事件数据:', event);
};
this.subscribeToEvents({
eventType: 'all',
importance: 'all',
onNewEvent: onNewEvent || defaultCallback,
onSubscribed: (data) => {
console.log('%c[socketService] ✅ 订阅成功!', 'color: #4CAF50; font-weight: bold;');
console.log('[socketService] 订阅确认:', data);
},
onNewEvent: onNewEvent || (() => {}),
});
// 返回取消订阅的清理函数

View File

@@ -1,26 +1,38 @@
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import communityDataReducer from './slices/communityDataSlice';
import posthogReducer from './slices/posthogSlice';
// ⚡ PostHog 延迟加载:移除同步导入,首屏减少 ~180KB
// import posthogReducer from './slices/posthogSlice';
// import posthogMiddleware from './middleware/posthogMiddleware';
import industryReducer from './slices/industrySlice';
import stockReducer from './slices/stockSlice';
import authModalReducer from './slices/authModalSlice';
import subscriptionReducer from './slices/subscriptionSlice';
import deviceReducer from './slices/deviceSlice'; // ✅ 设备检测状态管理
import posthogMiddleware from './middleware/posthogMiddleware';
import { eventsApi } from './api/eventsApi'; // ✅ RTK Query API
// ⚡ 基础 reducers首屏必需
const staticReducers = {
communityData: communityDataReducer,
industry: industryReducer, // ✅ 行业分类数据管理
stock: stockReducer, // ✅ 股票和事件数据管理
authModal: authModalReducer, // ✅ 认证弹窗状态管理
subscription: subscriptionReducer, // ✅ 订阅信息状态管理
device: deviceReducer, // ✅ 设备检测状态管理(移动端/桌面端)
[eventsApi.reducerPath]: eventsApi.reducer, // ✅ RTK Query 事件 API
};
// ⚡ 动态 reducers 注册表
const asyncReducers = {};
// ⚡ 创建根 reducer 的工厂函数
const createRootReducer = () => combineReducers({
...staticReducers,
...asyncReducers,
});
export const store = configureStore({
reducer: {
communityData: communityDataReducer,
posthog: posthogReducer, // ✅ PostHog Redux 状态管理
industry: industryReducer, // ✅ 行业分类数据管理
stock: stockReducer, // ✅ 股票和事件数据管理
authModal: authModalReducer, // ✅ 认证弹窗状态管理
subscription: subscriptionReducer, // ✅ 订阅信息状态管理
device: deviceReducer, // ✅ 设备检测状态管理(移动端/桌面端)
[eventsApi.reducerPath]: eventsApi.reducer, // ✅ RTK Query 事件 API
},
reducer: createRootReducer(),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
@@ -28,14 +40,27 @@ export const store = configureStore({
ignoredActions: [
'communityData/fetchPopularKeywords/fulfilled',
'communityData/fetchHotEvents/fulfilled',
'posthog/trackEvent/fulfilled', // ✅ PostHog 事件追踪
'posthog/trackEvent/fulfilled', // ✅ PostHog 事件追踪(延迟加载后仍需)
'stock/fetchEventStocks/fulfilled',
'stock/fetchStockQuotes/fulfilled',
],
},
})
.concat(posthogMiddleware) // PostHog 自动追踪中间件
// PostHog 中间件延迟加载,首屏不再需要
.concat(eventsApi.middleware), // ✅ RTK Query 中间件(自动缓存、去重、重试)
});
/**
* ⚡ 动态注入 reducer用于延迟加载模块
* @param {string} key - reducer 的键名
* @param {Function} reducer - reducer 函数
*/
export const injectReducer = (key, reducer) => {
if (asyncReducers[key]) {
return; // 已注入,避免重复
}
asyncReducers[key] = reducer;
store.replaceReducer(createRootReducer());
};
export default store;

View File

@@ -2,27 +2,81 @@
import { createSlice } from '@reduxjs/toolkit';
/**
* 检测当前设备是否为移动设备
*
* 判断逻辑:
* 1. User Agent 检测(移动设备标识)
* 2. 屏幕宽度检测(<= 768px
* 3. 触摸屏检测(支持触摸事件)
*
* @returns {boolean} true 表示移动设备false 表示桌面设备
* 设备类型枚举
*/
const detectIsMobile = () => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
const isMobileUA = mobileRegex.test(userAgent);
const isMobileWidth = window.innerWidth <= 768;
const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
return isMobileUA || (isMobileWidth && hasTouchScreen);
export const DeviceType = {
MOBILE: 'mobile',
TABLET: 'tablet',
DESKTOP: 'desktop',
};
/**
* 检测设备类型
*
* 判断逻辑:
* - Mobile: 手机设备iPhone、Android 手机等,宽度 <= 768px
* - Tablet: 平板设备iPad、Android 平板等,宽度 769px - 1024px
* - Desktop: 桌面设备PC、Mac 等,宽度 > 1024px
*
* @returns {{ isMobile: boolean, isTablet: boolean, isDesktop: boolean, deviceType: string }}
*/
const detectDeviceType = () => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
const screenWidth = window.innerWidth;
// iPad 检测(包括 iPadOS 13+ 的 Safari其 UA 不再包含 iPad
const isIPad =
/iPad/i.test(userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
// Android 平板检测Android 设备但 UA 不包含 Mobile
const isAndroidTablet = /Android/i.test(userAgent) && !/Mobile/i.test(userAgent);
// 手机 UA 检测(排除平板)
const mobileRegex = /iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i;
const isAndroidPhone = /Android/i.test(userAgent) && /Mobile/i.test(userAgent);
const isMobileUA = mobileRegex.test(userAgent) || isAndroidPhone;
// 触摸屏检测
const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// 综合判断设备类型
let isMobile = false;
let isTablet = false;
let isDesktop = false;
if (isIPad || isAndroidTablet) {
// UA 明确是平板
isTablet = true;
} else if (isMobileUA) {
// UA 明确是手机
isMobile = true;
} else if (screenWidth <= 768 && hasTouchScreen) {
// 小屏幕 + 触摸屏 = 手机
isMobile = true;
} else if (screenWidth > 768 && screenWidth <= 1024 && hasTouchScreen) {
// 中等屏幕 + 触摸屏 = 平板
isTablet = true;
} else {
// 其他情况为桌面设备
isDesktop = true;
}
// 确定设备类型字符串
let deviceType = DeviceType.DESKTOP;
if (isMobile) deviceType = DeviceType.MOBILE;
else if (isTablet) deviceType = DeviceType.TABLET;
return { isMobile, isTablet, isDesktop, deviceType };
};
const initialDeviceState = detectDeviceType();
const initialState = {
isMobile: detectIsMobile(),
isMobile: initialDeviceState.isMobile,
isTablet: initialDeviceState.isTablet,
isDesktop: initialDeviceState.isDesktop,
deviceType: initialDeviceState.deviceType,
};
const deviceSlice = createSlice({
@@ -37,7 +91,11 @@ const deviceSlice = createSlice({
* - 屏幕方向变化时调用orientationchange
*/
updateScreenSize: (state) => {
state.isMobile = detectIsMobile();
const { isMobile, isTablet, isDesktop, deviceType } = detectDeviceType();
state.isMobile = isMobile;
state.isTablet = isTablet;
state.isDesktop = isDesktop;
state.deviceType = deviceType;
},
},
});
@@ -47,6 +105,9 @@ export const { updateScreenSize } = deviceSlice.actions;
// Selectors
export const selectIsMobile = (state) => state.device.isMobile;
export const selectIsTablet = (state) => state.device.isTablet;
export const selectIsDesktop = (state) => state.device.isDesktop;
export const selectDeviceType = (state) => state.device.deviceType;
// Reducer
export default deviceSlice.reducer;

View File

@@ -7,26 +7,49 @@
* 3. selector 函数测试
*/
import deviceReducer, { updateScreenSize, selectIsMobile } from './deviceSlice';
import deviceReducer, {
updateScreenSize,
selectIsMobile,
selectIsTablet,
selectIsDesktop,
selectDeviceType,
DeviceType,
} from './deviceSlice';
describe('deviceSlice', () => {
describe('reducer', () => {
it('should return the initial state', () => {
const initialState = deviceReducer(undefined, { type: '@@INIT' });
expect(initialState).toHaveProperty('isMobile');
expect(initialState).toHaveProperty('isTablet');
expect(initialState).toHaveProperty('isDesktop');
expect(initialState).toHaveProperty('deviceType');
expect(typeof initialState.isMobile).toBe('boolean');
expect(typeof initialState.isTablet).toBe('boolean');
expect(typeof initialState.isDesktop).toBe('boolean');
expect(typeof initialState.deviceType).toBe('string');
});
it('should handle updateScreenSize', () => {
// 模拟初始状态
const initialState = { isMobile: false };
const initialState = {
isMobile: false,
isTablet: false,
isDesktop: true,
deviceType: DeviceType.DESKTOP,
};
// 执行 action注意实际 isMobile 值由 detectIsMobile() 决定)
// 执行 action注意实际值由 detectDeviceType() 决定)
const newState = deviceReducer(initialState, updateScreenSize());
// 验证状态结构
expect(newState).toHaveProperty('isMobile');
expect(newState).toHaveProperty('isTablet');
expect(newState).toHaveProperty('isDesktop');
expect(newState).toHaveProperty('deviceType');
expect(typeof newState.isMobile).toBe('boolean');
expect(typeof newState.isTablet).toBe('boolean');
expect(typeof newState.isDesktop).toBe('boolean');
});
});
@@ -35,22 +58,48 @@ describe('deviceSlice', () => {
const mockState = {
device: {
isMobile: true,
isTablet: false,
isDesktop: false,
deviceType: DeviceType.MOBILE,
},
};
const result = selectIsMobile(mockState);
expect(result).toBe(true);
expect(selectIsMobile(mockState)).toBe(true);
expect(selectIsTablet(mockState)).toBe(false);
expect(selectIsDesktop(mockState)).toBe(false);
expect(selectDeviceType(mockState)).toBe(DeviceType.MOBILE);
});
it('selectIsMobile should return false for desktop', () => {
it('selectIsTablet should return correct value', () => {
const mockState = {
device: {
isMobile: false,
isTablet: true,
isDesktop: false,
deviceType: DeviceType.TABLET,
},
};
const result = selectIsMobile(mockState);
expect(result).toBe(false);
expect(selectIsMobile(mockState)).toBe(false);
expect(selectIsTablet(mockState)).toBe(true);
expect(selectIsDesktop(mockState)).toBe(false);
expect(selectDeviceType(mockState)).toBe(DeviceType.TABLET);
});
it('selectIsDesktop should return correct value', () => {
const mockState = {
device: {
isMobile: false,
isTablet: false,
isDesktop: true,
deviceType: DeviceType.DESKTOP,
},
};
expect(selectIsMobile(mockState)).toBe(false);
expect(selectIsTablet(mockState)).toBe(false);
expect(selectIsDesktop(mockState)).toBe(true);
expect(selectDeviceType(mockState)).toBe(DeviceType.DESKTOP);
});
});
@@ -60,4 +109,12 @@ describe('deviceSlice', () => {
expect(action.type).toBe('device/updateScreenSize');
});
});
describe('DeviceType constants', () => {
it('should have correct values', () => {
expect(DeviceType.MOBILE).toBe('mobile');
expect(DeviceType.TABLET).toBe('tablet');
expect(DeviceType.DESKTOP).toBe('desktop');
});
});
});

View File

@@ -1,190 +0,0 @@
/**
* deviceSlice 使用示例
*
* 本文件展示如何在 React 组件中使用 deviceSlice 来实现响应式设计
*/
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { selectIsMobile, updateScreenSize } from '@/store/slices/deviceSlice';
import { Box, Text, VStack } from '@chakra-ui/react';
/**
* 示例 1: 基础使用 - 根据设备类型渲染不同内容
*/
export const BasicUsageExample = () => {
const isMobile = useSelector(selectIsMobile);
return (
<Box>
{isMobile ? (
<Text>📱 移动端视图</Text>
) : (
<Text>💻 桌面端视图</Text>
)}
</Box>
);
};
/**
* 示例 2: 监听窗口尺寸变化 - 动态更新设备状态
*/
export const ResizeListenerExample = () => {
const isMobile = useSelector(selectIsMobile);
const dispatch = useDispatch();
useEffect(() => {
// 监听窗口尺寸变化
const handleResize = () => {
dispatch(updateScreenSize());
};
// 监听屏幕方向变化(移动设备)
const handleOrientationChange = () => {
dispatch(updateScreenSize());
};
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleOrientationChange);
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleOrientationChange);
};
}, [dispatch]);
return (
<VStack>
<Text>当前设备: {isMobile ? '移动设备' : '桌面设备'}</Text>
<Text fontSize="sm" color="gray.500">
试试调整浏览器窗口大小
</Text>
</VStack>
);
};
/**
* 示例 3: 响应式布局 - 根据设备类型调整样式
*/
export const ResponsiveLayoutExample = () => {
const isMobile = useSelector(selectIsMobile);
return (
<Box
p={isMobile ? 4 : 8}
bg={isMobile ? 'blue.50' : 'gray.50'}
borderRadius={isMobile ? 'md' : 'xl'}
maxW={isMobile ? '100%' : '800px'}
mx="auto"
>
<Text fontSize={isMobile ? 'md' : 'lg'}>
响应式内容区域
</Text>
<Text fontSize="sm" color="gray.600" mt={2}>
Padding: {isMobile ? '16px' : '32px'}
</Text>
</Box>
);
};
/**
* 示例 4: 条件渲染组件 - 移动端显示简化版
*/
export const ConditionalRenderExample = () => {
const isMobile = useSelector(selectIsMobile);
return (
<Box>
{isMobile ? (
// 移动端:简化版导航栏
<Box bg="blue.500" p={2}>
<Text color="white" fontSize="sm"> 菜单</Text>
</Box>
) : (
// 桌面端:完整导航栏
<Box bg="blue.500" p={4}>
<Text color="white" fontSize="lg">
首页 | 产品 | 关于我们 | 联系方式
</Text>
</Box>
)}
</Box>
);
};
/**
* 示例 5: 在 App.js 中全局监听(推荐方式)
*
* 将以下代码添加到 src/App.js 中:
*/
export const AppLevelResizeListenerExample = () => {
const dispatch = useDispatch();
useEffect(() => {
const handleResize = () => {
dispatch(updateScreenSize());
};
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
// 初始化时也调用一次(可选)
handleResize();
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
};
}, [dispatch]);
// 返回 null 或组件内容
return null;
};
/**
* 示例 6: 自定义 Hook 封装(推荐)
*
* 在 src/hooks/useDevice.js 中创建自定义 Hook
*/
// import { useSelector } from 'react-redux';
// import { selectIsMobile } from '@/store/slices/deviceSlice';
//
// export const useDevice = () => {
// const isMobile = useSelector(selectIsMobile);
//
// return {
// isMobile,
// isDesktop: !isMobile,
// };
// };
/**
* 使用自定义 Hook
*/
export const CustomHookUsageExample = () => {
// const { isMobile, isDesktop } = useDevice();
return (
<Box>
{/* <Text>移动设备: {isMobile ? '是' : '否'}</Text> */}
{/* <Text>桌面设备: {isDesktop ? '是' : '否'}</Text> */}
</Box>
);
};
/**
* 推荐实践:
*
* 1. 在 App.js 中添加全局 resize 监听器
* 2. 创建自定义 Hook (useDevice) 简化使用
* 3. 结合 Chakra UI 的响应式 Props优先使用 Chakra 内置响应式)
* 4. 仅在需要 JS 逻辑判断时使用 Redux如条件渲染、动态导入
*
* Chakra UI 响应式示例(推荐优先使用):
* <Box
* fontSize={{ base: 'sm', md: 'md', lg: 'lg' }} // Chakra 内置响应式
* p={{ base: 4, md: 6, lg: 8 }}
* >
* 内容
* </Box>
*/

View File

@@ -12,6 +12,19 @@ import {
} from '../../lib/posthog';
import { logger } from '../../utils/logger';
// ⚡ 模块级缓存:存储 reducer 注入前的事件(避免丢失)
let pendingEventsBeforeInjection = [];
/**
* 获取并清空注入前缓存的事件
* 在 App.js 中 reducer 注入后调用
*/
export const flushPendingEventsBeforeInjection = () => {
const events = [...pendingEventsBeforeInjection];
pendingEventsBeforeInjection = [];
return events;
};
// ==================== Initial State ====================
const initialState = {
@@ -51,7 +64,15 @@ export const initializePostHog = createAsyncThunk(
'posthog/initialize',
async (_, { getState, rejectWithValue }) => {
try {
const { config } = getState().posthog;
const posthogState = getState().posthog;
// ⚡ 防御性检查reducer 尚未注入
if (!posthogState) {
logger.warn('PostHog', 'PostHog reducer 尚未注入,跳过初始化');
return { isInitialized: false, skipped: true };
}
const { config } = posthogState;
if (!config.apiKey) {
logger.warn('PostHog', '未配置 API Key分析功能将被禁用');
@@ -112,7 +133,20 @@ export const trackEvent = createAsyncThunk(
'posthog/trackEvent',
async ({ eventName, properties = {} }, { getState, rejectWithValue }) => {
try {
const { isInitialized } = getState().posthog;
const posthogState = getState().posthog;
// ⚡ reducer 尚未注入:缓存到模块级队列(不丢弃)
if (!posthogState) {
logger.debug('PostHog', 'PostHog reducer 尚未注入,事件已缓存', { eventName });
pendingEventsBeforeInjection.push({
eventName,
properties,
timestamp: new Date().toISOString()
});
return { eventName, properties, pendingInjection: true };
}
const { isInitialized } = posthogState;
if (!isInitialized) {
logger.warn('PostHog', 'PostHog 未初始化,事件将被缓存', { eventName });
@@ -160,7 +194,14 @@ export const flushCachedEvents = createAsyncThunk(
'posthog/flushCachedEvents',
async (_, { getState, dispatch }) => {
try {
const { eventQueue, isInitialized } = getState().posthog;
const posthogState = getState().posthog;
// ⚡ 防御性检查reducer 尚未注入
if (!posthogState) {
return { flushed: 0, skipped: true };
}
const { eventQueue, isInitialized } = posthogState;
if (!isInitialized || eventQueue.length === 0) {
return { flushed: 0 };
@@ -281,15 +322,16 @@ export const {
// ==================== Selectors ====================
export const selectPostHog = (state) => state.posthog;
export const selectIsInitialized = (state) => state.posthog.isInitialized;
export const selectUser = (state) => state.posthog.user;
export const selectFeatureFlags = (state) => state.posthog.featureFlags;
export const selectEventQueue = (state) => state.posthog.eventQueue;
export const selectStats = (state) => state.posthog.stats;
// ⚡ 安全的 selectors支持 reducer 未注入的情况)
export const selectPostHog = (state) => state.posthog || initialState;
export const selectIsInitialized = (state) => state.posthog?.isInitialized ?? false;
export const selectUser = (state) => state.posthog?.user ?? null;
export const selectFeatureFlags = (state) => state.posthog?.featureFlags ?? {};
export const selectEventQueue = (state) => state.posthog?.eventQueue ?? [];
export const selectStats = (state) => state.posthog?.stats ?? initialState.stats;
export const selectFeatureFlag = (flagKey) => (state) => {
return state.posthog.featureFlags[flagKey] || posthogGetFeatureFlag(flagKey);
return state.posthog?.featureFlags?.[flagKey] || posthogGetFeatureFlag(flagKey);
};
export const selectIsOptedOut = () => posthogHasOptedOut();

View File

@@ -7,6 +7,7 @@ import { getApiBase } from '../../utils/apiConfig';
/**
* 异步 Thunk: 获取用户订阅信息
* 使用 condition 选项防止同一时刻多个组件重复发起请求
*/
export const fetchSubscriptionInfo = createAsyncThunk(
'subscription/fetchInfo',
@@ -51,6 +52,21 @@ export const fetchSubscriptionInfo = createAsyncThunk(
logger.error('subscriptionSlice', '加载订阅信息失败', error);
return rejectWithValue(error.message);
}
},
{
// 防止重复请求:如果已加载或正在加载中,则跳过本次请求
condition: (_, { getState }) => {
const { subscription } = getState();
// 如果正在加载或已加载完成,返回 false 阻止请求
if (subscription.loading || subscription.loaded) {
logger.debug('subscriptionSlice', '跳过重复请求', {
loading: subscription.loading,
loaded: subscription.loaded
});
return false;
}
return true;
}
}
);
@@ -71,6 +87,7 @@ const subscriptionSlice = createSlice({
},
// 加载状态
loading: false,
loaded: false, // 是否已加载过(用于防止重复请求)
error: null,
// 订阅 Modal 状态
isModalOpen: false,
@@ -104,8 +121,8 @@ const subscriptionSlice = createSlice({
end_date: null
};
state.loading = false;
state.loaded = false; // 重置已加载标记,下次登录时重新获取
state.error = null;
logger.debug('subscriptionSlice', '重置订阅信息为免费版');
},
},
extraReducers: (builder) => {
@@ -118,6 +135,7 @@ const subscriptionSlice = createSlice({
// fetchSubscriptionInfo - fulfilled
.addCase(fetchSubscriptionInfo.fulfilled, (state, action) => {
state.loading = false;
state.loaded = true; // 标记已加载
state.info = action.payload;
state.error = null;
})
@@ -136,6 +154,7 @@ export const { openModal, closeModal, resetToFree } = subscriptionSlice.actions;
// 导出 selectors
export const selectSubscriptionInfo = (state) => state.subscription.info;
export const selectSubscriptionLoading = (state) => state.subscription.loading;
export const selectSubscriptionLoaded = (state) => state.subscription.loaded;
export const selectSubscriptionError = (state) => state.subscription.error;
export const selectSubscriptionModalOpen = (state) => state.subscription.isModalOpen;

View File

@@ -1,12 +0,0 @@
/* Tailwind CSS 入口文件 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 自定义工具类 */
@layer utilities {
/* 毛玻璃效果 */
.backdrop-blur-xl {
backdrop-filter: blur(24px);
}
}

View File

@@ -36,3 +36,37 @@ iframe[src*="/visitor/"] {
[class*="bytedesk-badge"] {
z-index: 1000000 !important;
}
/* ========== H5 端客服组件整体缩小 ========== */
@media (max-width: 768px) {
/* 整个客服容器缩小(包括按钮和提示框) */
[class*="bytedesk"],
[id*="bytedesk"],
[class*="BytedeskWeb"] {
transform: scale(0.7) !important;
transform-origin: bottom right !important;
}
}
/* ========== 提示框 3 秒后自动消失 ========== */
/* 提示框("在线客服 点击咨询"气泡)- 扩展选择器 */
[class*="bytedesk-bubble"],
[class*="bytedesk-tooltip"],
[class*="BytedeskWeb"] [class*="bubble"],
[class*="BytedeskWeb"] [class*="tooltip"],
[class*="bytedesk"] > div:not(button):not(iframe),
[class*="BytedeskWeb"] > div:not(button):not(iframe),
[id*="bytedesk"] > div:not(button):not(iframe) {
animation: bytedeskFadeOut 0.3s ease-out 3s forwards !important;
}
@keyframes bytedeskFadeOut {
from {
opacity: 1;
visibility: visible;
}
to {
opacity: 0;
visibility: hidden;
}
}

33
src/types/home.ts Normal file
View File

@@ -0,0 +1,33 @@
// src/types/home.ts
// HomePage 相关类型定义
/**
* 功能特性配置
*/
export interface Feature {
id: string;
title: string;
description: string;
icon: string;
color: string;
url: string;
badge: string;
featured?: boolean;
}
/**
* 响应式配置
*/
export interface ResponsiveConfig {
heroHeight: string | undefined;
headingSize: string | undefined;
headingLetterSpacing: string | undefined;
heroTextSize: string | undefined;
containerPx: number | undefined;
showDecorations: boolean | undefined;
}
/**
* 功能卡片点击处理函数类型
*/
export type FeatureClickHandler = (feature: Feature) => void;

44
src/types/static-assets.d.ts vendored Normal file
View File

@@ -0,0 +1,44 @@
/**
* 静态资源模块声明
* 允许 TypeScript 正确处理静态资源导入
*/
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.jpeg' {
const content: string;
export default content;
}
declare module '*.gif' {
const content: string;
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.webp' {
const content: string;
export default content;
}
declare module '*.ico' {
const content: string;
export default content;
}
declare module '*.bmp' {
const content: string;
export default content;
}

View File

@@ -7,6 +7,34 @@ const isDevelopment =
process.env.NODE_ENV === 'development' ||
process.env.REACT_APP_ENABLE_DEBUG === 'true';
// ========== 日志级别配置 ==========
// 日志级别error < warn < info < debug
// 默认级别warn只显示警告和错误
// 可通过 localStorage.setItem('LOG_LEVEL', 'debug') 开启详细日志
const LOG_LEVELS = {
error: 0,
warn: 1,
info: 2,
debug: 3,
};
// 从 localStorage 读取日志级别(允许用户临时开启详细日志)
const getLogLevel = () => {
if (typeof window !== 'undefined' && window.localStorage) {
const level = localStorage.getItem('LOG_LEVEL');
if (level && LOG_LEVELS[level] !== undefined) {
return LOG_LEVELS[level];
}
}
// 默认只显示 warn 和 error
return LOG_LEVELS.warn;
};
// 检查是否应该输出指定级别的日志
const shouldLogLevel = (level) => {
return LOG_LEVELS[level] <= getLogLevel();
};
// ========== 日志限流配置 ==========
const LOG_THROTTLE_TIME = 1000; // 1秒内相同日志只输出一次
const recentLogs = new Map(); // 日志缓存,用于去重
@@ -148,13 +176,13 @@ export const logger = {
},
/**
* 调试日志(仅开发环境)
* 调试日志(仅开发环境 + LOG_LEVEL=debug
* @param {string} component - 组件名称
* @param {string} message - 调试信息
* @param {object} data - 相关数据(可选)
*/
debug: (component, message, data = {}) => {
if (isDevelopment && shouldLog(component, message)) {
if (isDevelopment && shouldLogLevel('debug') && shouldLog(component, message)) {
console.group(`🐛 Debug: ${component}`);
console.log('Message:', message);
if (Object.keys(data).length > 0) {
@@ -166,13 +194,13 @@ export const logger = {
},
/**
* 信息日志(仅开发环境)
* 信息日志(仅开发环境 + LOG_LEVEL>=info
* @param {string} component - 组件名称
* @param {string} message - 信息内容
* @param {object} data - 相关数据(可选)
*/
info: (component, message, data = {}) => {
if (isDevelopment && shouldLog(component, message)) {
if (isDevelopment && shouldLogLevel('info') && shouldLog(component, message)) {
console.group(` Info: ${component}`);
console.log('Message:', message);
if (Object.keys(data).length > 0) {
@@ -181,6 +209,28 @@ export const logger = {
console.log('Timestamp:', new Date().toISOString());
console.groupEnd();
}
},
/**
* 设置日志级别(方便调试)
* @param {string} level - 日志级别 ('error' | 'warn' | 'info' | 'debug')
*/
setLevel: (level) => {
if (LOG_LEVELS[level] !== undefined) {
localStorage.setItem('LOG_LEVEL', level);
console.log(`[Logger] 日志级别已设置为: ${level}`);
console.log(`[Logger] 可用级别: error < warn < info < debug`);
} else {
console.error(`[Logger] 无效的日志级别: ${level}`);
}
},
/**
* 获取当前日志级别
*/
getLevel: () => {
const levelNum = getLogLevel();
return Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === levelNum) || 'warn';
}
};

View File

@@ -58,11 +58,6 @@ const performanceMeasures: Array<{ name: string; duration: number; startMark: st
*/
class PerformanceMonitor {
private metrics: PerformanceMetrics = {};
private isProduction: boolean;
constructor() {
this.isProduction = process.env.NODE_ENV === 'production';
}
/**
* 标记性能时间点
@@ -70,12 +65,6 @@ class PerformanceMonitor {
mark(name: string): void {
const timestamp = performance.now();
performanceMarks.set(name, timestamp);
if (!this.isProduction) {
logger.debug('PerformanceMonitor', `⏱️ Mark: ${name}`, {
time: `${timestamp.toFixed(2)}ms`
});
}
}
/**
@@ -106,12 +95,6 @@ class PerformanceMonitor {
endMark
});
if (!this.isProduction) {
logger.debug('PerformanceMonitor', `📊 Measure: ${measureName}`, {
duration: `${duration.toFixed(2)}ms`
});
}
return duration;
}

View File

@@ -4,7 +4,6 @@
import { useState, useCallback } from 'react';
import type { Dispatch, SetStateAction, KeyboardEvent } from 'react';
import axios from 'axios';
import { logger } from '@utils/logger';
import { MessageTypes, type Message } from '../constants/messageTypes';
import type { UploadedFile } from './useFileUpload';
import type { User } from './useAgentSessions';
@@ -221,7 +220,7 @@ export const useAgentChat = ({
loadSessions();
}
} catch (error: any) {
logger.error('Agent chat error', error);
console.error('Agent chat error:', error);
// 移除 "思考中" 和 "执行中" 消息
setMessages((prev) =>

View File

@@ -4,7 +4,6 @@
import { useState, useEffect, useCallback } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import axios from 'axios';
import { logger } from '@utils/logger';
import { MessageTypes, type Message } from '../constants/messageTypes';
/**
@@ -103,7 +102,7 @@ export const useAgentSessions = ({
setSessions(response.data.data);
}
} catch (error) {
logger.error('加载会话列表失败', error);
console.error('加载会话列表失败:', error);
} finally {
setIsLoadingSessions(false);
}
@@ -135,7 +134,7 @@ export const useAgentSessions = ({
setMessages(formattedMessages);
}
} catch (error) {
logger.error('加载会话历史失败', error);
console.error('加载会话历史失败:', error);
}
},
[setMessages]

View File

@@ -0,0 +1,80 @@
/* CompactSearchBox.css */
/* 紧凑版搜索和筛选组件样式 */
/* 搜索框 placeholder 白色 - 全覆盖选择器 */
.gold-placeholder input::placeholder,
.gold-placeholder input[type="text"]::placeholder,
.gold-placeholder .ant-input::placeholder,
.gold-placeholder .ant-input-affix-wrapper input::placeholder,
.gold-placeholder .ant-select-selection-search-input::placeholder,
.gold-placeholder .ant-input-affix-wrapper .ant-input::placeholder {
color: #FFFFFF !important;
opacity: 0.8 !important;
}
/* AutoComplete placeholder - 关键选择器 */
.gold-placeholder .ant-select-selection-placeholder {
color: #FFFFFF !important;
opacity: 0.8 !important;
}
.gold-placeholder .ant-input-affix-wrapper .ant-input,
.gold-placeholder .ant-input {
color: #FFFFFF !important;
}
.gold-placeholder .ant-input-affix-wrapper {
background: transparent !important;
}
/* 透明下拉框样式 */
.transparent-select .ant-select-selector,
.transparent-cascader .ant-select-selector {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
/* 行业筛选宽度自适应,减少间距 */
.transparent-cascader {
width: auto !important;
}
.transparent-cascader .ant-select-selector {
padding-right: 8px !important;
min-width: unset !important;
}
/* 行业筛选 Cascader placeholder 白色 */
.transparent-select .ant-select-selection-placeholder,
.transparent-cascader .ant-select-selection-placeholder,
.transparent-cascader input::placeholder,
.transparent-cascader .ant-cascader-input::placeholder {
color: #FFFFFF !important;
}
.transparent-cascader .ant-cascader-input {
background: transparent !important;
}
/* 行业筛选 Cascader 选中值白色 */
.transparent-cascader .ant-select-selection-item,
.transparent-cascader .ant-cascader-picker-label {
color: #FFFFFF !important;
}
/* 方括号样式下拉框 - 无边框 */
.bracket-select .ant-select-selector {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
.bracket-select .ant-select-selection-item,
.bracket-select .ant-select-selection-placeholder {
color: #FFFFFF !important;
}
.bracket-select .ant-select-arrow {
color: rgba(255, 255, 255, 0.65) !important;
}

View File

@@ -4,30 +4,49 @@
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import {
Input, Cascader, Button, Space, Tag, AutoComplete, Select as AntSelect,
Tooltip
Tooltip, Divider, Flex
} from 'antd';
import {
SearchOutlined, CloseCircleOutlined, StockOutlined, FilterOutlined,
CalendarOutlined, SortAscendingOutlined
CalendarOutlined, SortAscendingOutlined, ReloadOutlined, ThunderboltOutlined
} from '@ant-design/icons';
import dayjs from 'dayjs';
import debounce from 'lodash/debounce';
import { useSelector, useDispatch } from 'react-redux';
import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '../../../store/slices/industrySlice';
import { stockService } from '../../../services/stockService';
import { logger } from '../../../utils/logger';
import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '@store/slices/industrySlice';
import { stockService } from '@services/stockService';
import { logger } from '@utils/logger';
import TradingTimeFilter from './TradingTimeFilter';
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
import './CompactSearchBox.css';
const { Option } = AntSelect;
// 排序选项常量
const SORT_OPTIONS = [
{ value: 'new', label: '最新排序', mobileLabel: '最新' },
{ value: 'hot', label: '最热排序', mobileLabel: '热门' },
{ value: 'importance', label: '重要性排序', mobileLabel: '重要' },
{ value: 'returns_avg', label: '平均收益', mobileLabel: '均收' },
{ value: 'returns_week', label: '周收益', mobileLabel: '周收' },
];
// 重要性等级常量
const IMPORTANCE_OPTIONS = [
{ value: 'S', label: 'S级' },
{ value: 'A', label: 'A级' },
{ value: 'B', label: 'B级' },
{ value: 'C', label: 'C级' },
];
const CompactSearchBox = ({
onSearch,
onSearchFocus,
filters = {},
mode,
pageSize,
trackingFunctions = {}
trackingFunctions = {},
isMobile = false
}) => {
// 状态
const [stockOptions, setStockOptions] = useState([]);
@@ -420,19 +439,21 @@ const CompactSearchBox = ({
dispatch(fetchIndustryData());
}
};
return (
<div style={{
padding: window.innerWidth < 768 ? '12px 16px' : '16px 20px',
background: PROFESSIONAL_COLORS.background.card,
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3), 0 0 20px rgba(255, 195, 0, 0.1)',
border: `1px solid ${PROFESSIONAL_COLORS.border.default}`,
backdropFilter: 'blur(10px)'
}}>
{/* 单行紧凑布局 - 移动端自动换行 */}
<Space wrap style={{ width: '100%' }} size={window.innerWidth < 768 ? 'small' : 'medium'}>
{/* 搜索框 */}
<div style={{ padding: 0, background: 'transparent' }}>
{/* 第一行:搜索框 + 日期筛选 */}
<Flex
align="center"
gap={isMobile ? 8 : 12}
style={{
background: 'rgba(255, 255, 255, 0.03)',
border: `1px solid ${PROFESSIONAL_COLORS.border.light}`,
borderRadius: '24px',
padding: isMobile ? '2px 4px' : '8px 16px',
marginBottom: isMobile ? 8 : 12
}}
>
{/* 搜索框 - flex: 1 占满剩余空间 */}
<AutoComplete
value={inputValue}
onChange={handleInputChange}
@@ -440,46 +461,57 @@ const CompactSearchBox = ({
onSelect={handleStockSelect}
onFocus={onSearchFocus}
options={stockOptions}
placeholder="搜索股票/话题..."
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleMainSearch();
}
}}
style={{ width: window.innerWidth < 768 ? '100%' : 240, minWidth: window.innerWidth < 768 ? 0 : 240 }}
style={{ flex: 1, minWidth: isMobile ? 100 : 200 }}
className="gold-placeholder"
>
<Input
prefix={<SearchOutlined style={{ color: PROFESSIONAL_COLORS.gold[500] }} />}
placeholder="搜索股票/话题..."
style={{
borderRadius: '8px',
border: `1px solid ${PROFESSIONAL_COLORS.border.default}`,
boxShadow: `0 2px 8px rgba(255, 195, 0, 0.1)`,
background: PROFESSIONAL_COLORS.background.secondary,
color: PROFESSIONAL_COLORS.text.primary
border: 'none',
background: 'transparent',
color: PROFESSIONAL_COLORS.text.primary,
boxShadow: 'none'
}}
/>
</AutoComplete>
{/* 时间筛选 */}
<Tooltip title="时间筛选">
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<CalendarOutlined style={{ color: PROFESSIONAL_COLORS.gold[500], fontSize: 12 }} />
<TradingTimeFilter
value={tradingTimeRange?.key || null}
onChange={handleTradingTimeChange}
compact
/>
</div>
</Tooltip>
{/* 分隔线 - H5 时隐藏 */}
{!isMobile && <Divider type="vertical" style={{ height: 24, margin: '0 8px', borderColor: 'rgba(255,255,255,0.15)' }} />}
{/* 行业筛选 */}
<Tooltip title="行业分类">
{/* 日期筛选按钮组 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 0 }}>
<CalendarOutlined style={{ color: PROFESSIONAL_COLORS.gold[500], fontSize: 14, marginRight: 8 }} />
<TradingTimeFilter
value={tradingTimeRange?.key || null}
onChange={handleTradingTimeChange}
compact={!isMobile}
mobile={isMobile}
/>
</div>
</Flex>
{/* 第二行:筛选条件 */}
<Flex justify="space-between" align="center">
{/* 左侧筛选 */}
<Space size={isMobile ? 4 : 8}>
{/* 行业筛选 */}
<Cascader
value={industryValue}
onChange={handleIndustryChange}
onFocus={handleCascaderFocus}
options={industryData || []}
placeholder="行业"
placeholder={
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<FilterOutlined style={{ fontSize: 12 }} />
{isMobile ? '行业' : '行业筛选'}
</span>
}
changeOnSelect
showSearch={{
filter: (inputValue, path) =>
@@ -489,145 +521,65 @@ const CompactSearchBox = ({
}}
allowClear
expandTrigger="hover"
displayRender={(labels) => labels[labels.length - 1] || '行业'}
displayRender={(labels) => labels[labels.length - 1] || (isMobile ? '行业' : '行业筛选')}
disabled={industryLoading}
style={{
width: window.innerWidth < 768 ? '100%' : 120,
minWidth: window.innerWidth < 768 ? 0 : 120,
borderRadius: '8px'
}}
suffixIcon={<FilterOutlined style={{ fontSize: 14, color: PROFESSIONAL_COLORS.gold[500] }} />}
style={{ minWidth: isMobile ? 70 : 80 }}
suffixIcon={null}
className="transparent-cascader"
/>
</Tooltip>
{/* 重要性筛选 */}
<Tooltip title="事件等级筛选">
{/* 事件等级 */}
<AntSelect
mode="multiple"
value={importance}
onChange={handleImportanceChange}
style={{
width: window.innerWidth < 768 ? '100%' : 120,
minWidth: window.innerWidth < 768 ? 0 : 120,
borderRadius: '8px'
}}
placeholder="事件等级"
style={{ minWidth: isMobile ? 100 : 120 }}
placeholder={
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<ThunderboltOutlined style={{ fontSize: 12 }} />
{isMobile ? '等级' : '事件等级'}
</span>
}
maxTagCount={0}
maxTagPlaceholder={(omittedValues) => `已选 ${omittedValues.length}`}
maxTagPlaceholder={(omittedValues) => isMobile ? `${omittedValues.length}` : `已选 ${omittedValues.length}`}
className="bracket-select"
>
<Option value="S">S级</Option>
<Option value="A">A级</Option>
<Option value="B">B级</Option>
<Option value="C">C级</Option>
{IMPORTANCE_OPTIONS.map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</AntSelect>
</Tooltip>
</Space>
{/* 排序 */}
<Tooltip title="排序方式">
{/* 右侧排序和重置 */}
<Space size={isMobile ? 4 : 8}>
{/* 排序 */}
<AntSelect
value={sort}
onChange={handleSortChange}
style={{
width: window.innerWidth < 768 ? '100%' : 130,
minWidth: window.innerWidth < 768 ? 0 : 130,
borderRadius: '8px'
}}
suffixIcon={<SortAscendingOutlined style={{ fontSize: 14, color: PROFESSIONAL_COLORS.gold[500] }} />}
style={{ minWidth: isMobile ? 55 : 120 }}
className="bracket-select"
>
<Option value="new"> 最新</Option>
<Option value="hot">🔥 最热</Option>
<Option value="importance"> 重要性</Option>
<Option value="returns_avg">📊 平均收益</Option>
<Option value="returns_week">📈 周收益</Option>
{SORT_OPTIONS.map(opt => (
<Option key={opt.value} value={opt.value}>
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<SortAscendingOutlined style={{ fontSize: 12 }} />
{isMobile ? opt.mobileLabel : opt.label}
</span>
</Option>
))}
</AntSelect>
</Tooltip>
{/* 重置按钮 */}
<Tooltip title="重置所有筛选">
{/* 重置按钮 */}
<Button
icon={<CloseCircleOutlined />}
icon={<ReloadOutlined />}
onClick={handleReset}
danger
type="primary"
style={{
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(255, 77, 79, 0.2)'
}}
type="text"
style={{ color: PROFESSIONAL_COLORS.text.secondary }}
>
重置
{!isMobile && '重置筛选'}
</Button>
</Tooltip>
</Space>
{/* 激活的筛选标签(如果有的话) */}
{(inputValue || industryValue.length > 0 || importance.length > 0 || tradingTimeRange || sort !== 'new') && (
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 4 }}>
{inputValue && (
<Tag closable onClose={() => {
setInputValue('');
const params = buildFilterParams({ q: '' });
triggerSearch(params);
}} color="blue">
搜索: {inputValue}
</Tag>
)}
{tradingTimeRange && (
<Tag closable onClose={() => {
setTradingTimeRange(null);
const params = buildFilterParams({
start_date: '',
end_date: '',
recent_days: ''
});
triggerSearch(params);
}} color="green">
{tradingTimeRange.label}
</Tag>
)}
{industryValue.length > 0 && industryData && (
<Tag closable onClose={() => {
setIndustryValue([]);
const params = buildFilterParams({ industry_code: '' });
triggerSearch(params);
}} color="orange">
行业: {(() => {
const findLabel = (code, data) => {
for (const item of data) {
if (code.startsWith(item.value)) {
if (item.value === code) {
return item.label;
} else {
return findLabel(code, item.children);
}
}
}
return null;
};
const lastLevelCode = industryValue[industryValue.length - 1];
return findLabel(lastLevelCode, industryData);
})()}
</Tag>
)}
{importance.length > 0 && (
<Tag closable onClose={() => {
setImportance([]);
const params = buildFilterParams({ importance: 'all' });
triggerSearch(params);
}} color="purple">
重要性: {importance.map(imp => ({ 'S': '极高', 'A': '高', 'B': '中', 'C': '低' }[imp] || imp)).join(', ')}
</Tag>
)}
{sort && sort !== 'new' && (
<Tag closable onClose={() => {
setSort('new');
const params = buildFilterParams({ sort: 'new' });
triggerSearch(params);
}} color="cyan">
排序: {({ 'hot': '最热', 'importance': '重要性', 'returns_avg': '平均收益', 'returns_week': '周收益' }[sort] || sort)}
</Tag>
)}
</div>
)}
</Space>
</Flex>
</div>
);
};

View File

@@ -47,6 +47,7 @@ import { usePagination } from './DynamicNewsCard/hooks/usePagination';
import { PAGINATION_CONFIG, DISPLAY_MODES, REFRESH_DEBOUNCE_DELAY } from './DynamicNewsCard/constants';
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
import { debounce } from '../../../utils/debounce';
import { useDevice } from '@hooks/useDevice';
// 🔍 调试:渲染计数器
let dynamicNewsCardRenderCount = 0;
@@ -81,6 +82,7 @@ const DynamicNewsCardComponent = forwardRef(({
// 通知权限相关
const { browserPermission, requestBrowserPermission } = useNotification();
const { isMobile } = useDevice();
// Refs
const cardHeaderRef = useRef(null);
@@ -534,73 +536,53 @@ const [currentMode, setCurrentMode] = useState('vertical');
position="relative"
zIndex={1}
pb={3}
px={isMobile ? 2 : undefined}
>
<VStack spacing={3} align="stretch">
{/* 第一行:标题 + 通知开关 + 更新时间 */}
{/* 第一行:标题 + 模式切换 + 通知开关 + 更新时间 */}
<Flex justify="space-between" align="center">
{/* 左侧:标题 */}
<Heading size="md" color={PROFESSIONAL_COLORS.text.primary}>
<HStack spacing={2}>
<TimeIcon color={PROFESSIONAL_COLORS.gold[500]} />
<Text bgGradient={PROFESSIONAL_COLORS.gradients.gold} bgClip="text">实时要闻·动态追踪</Text>
</HStack>
</Heading>
{/* 左侧:标题 + 模式切换按钮 */}
<HStack spacing={4}>
<Heading size={isMobile ? "sm" : "md"} color={PROFESSIONAL_COLORS.text.primary}>
<HStack spacing={2}>
<TimeIcon color={PROFESSIONAL_COLORS.gold[500]} />
<Text bgGradient={PROFESSIONAL_COLORS.gradients.gold} bgClip="text">实时要闻·动态追踪</Text>
</HStack>
</Heading>
{/* 模式切换按钮(移动端隐藏) */}
{!isMobile && <ModeToggleButtons mode={mode} onModeChange={handleModeToggle} />}
</HStack>
{/* 右侧:通知开关 + 更新时间 */}
<HStack spacing={3}>
{/* 通知开关 */}
<Tooltip
label={browserPermission === 'granted'
? '浏览器通知已开启'
: '开启实时推送通知'}
placement="left"
hasArrow
>
{/* 通知开关 - 移动端隐藏 */}
{!isMobile && (
<HStack
spacing={2}
px={3}
py={1.5}
borderRadius="md"
bg={browserPermission === 'granted'
? useColorModeValue('green.50', 'green.900')
: useColorModeValue('gray.50', 'gray.700')}
borderWidth="1px"
borderColor={browserPermission === 'granted'
? useColorModeValue('green.200', 'green.700')
: useColorModeValue('gray.200', 'gray.600')}
cursor="pointer"
_hover={{
borderColor: browserPermission === 'granted'
? useColorModeValue('green.300', 'green.600')
: useColorModeValue('blue.300', 'blue.600'),
}}
transition="all 0.2s"
onClick={handleNotificationToggle}
_hover={{ opacity: 0.8 }}
transition="opacity 0.2s"
>
<Icon
as={BellIcon}
boxSize={3.5}
color={browserPermission === 'granted'
? useColorModeValue('green.600', 'green.300')
: useColorModeValue('gray.500', 'gray.400')}
color={PROFESSIONAL_COLORS.gold[500]}
/>
<Text
fontSize="sm"
fontWeight="medium"
color={browserPermission === 'granted'
? useColorModeValue('green.700', 'green.200')
: useColorModeValue('gray.600', 'gray.300')}
color={PROFESSIONAL_COLORS.text.secondary}
>
{browserPermission === 'granted' ? '已开启' : '开启通知'}
实时消息推送{browserPermission === 'granted' ? '已开启' : '开启'}
</Text>
<Switch
size="sm"
isChecked={browserPermission === 'granted'}
pointerEvents="none"
colorScheme="green"
colorScheme="yellow"
/>
</HStack>
</Tooltip>
)}
{/* 更新时间 */}
<Text fontSize="xs" color={PROFESSIONAL_COLORS.text.secondary} whiteSpace="nowrap">
@@ -618,6 +600,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
mode={mode}
pageSize={pageSize}
trackingFunctions={trackingFunctions}
isMobile={isMobile}
/>
</Box>
</VStack>
@@ -627,41 +610,14 @@ const [currentMode, setCurrentMode] = useState('vertical');
<CardBody
ref={cardBodyRef}
position="relative"
pt={4}
pt={0}
px={0}
mx={0}
display="flex"
flexDirection="column"
overflow="visible"
zIndex={1}
>
{/* 顶部控制栏:模式切换按钮 + 分页控制器(滚动时固定在顶部) */}
<Box
position="sticky"
top="0"
zIndex={10}
bg={cardBg}
py={2}
mb={2}
borderBottom="1px solid"
borderColor={borderColor}
mx={-6}
px={6}
boxShadow="sm"
>
<Flex justify="space-between" align="center">
{/* 左侧:模式切换按钮 */}
<ModeToggleButtons mode={mode} onModeChange={handleModeToggle} />
{/* 右侧:分页控制器(仅在纵向模式显示) */}
{mode === 'vertical' && totalPages > 1 && (
<PaginationControl
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChangeWithScroll}
/>
)}
</Flex>
</Box>
{/* 内容区域 - 撑满剩余高度 */}
<Box flex="1" minH={0} position="relative">
{/* Loading 蒙层 - 数据请求时显示 */}
@@ -727,6 +683,15 @@ const [currentMode, setCurrentMode] = useState('vertical');
</ModalContent>
</Modal>
)}
{/* 右侧分页控制器仅在纵向模式显示H5 放不下时折行 */}
{mode === 'vertical' && totalPages > 1 && (
<PaginationControl
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChangeWithScroll}
/>
)}
</Card>
);
});

View File

@@ -87,7 +87,7 @@ const EventScrollList = React.memo(({
h="100%"
pt={0}
pb={4}
px={mode === 'four-row' ? 0 : 2}
px={mode === 'four-row' ? 0 : { base: 0, md: 2 }}
position="relative"
data-scroll-container="true"
css={{

View File

@@ -17,7 +17,7 @@ const ModeToggleButtons = React.memo(({ mode, onModeChange }) => {
colorScheme="blue"
variant={mode === 'vertical' ? 'solid' : 'outline'}
>
纵向
列表
</Button>
<Button
onClick={() => onModeChange('four-row')}

View File

@@ -9,18 +9,12 @@ import {
Center,
Text,
useBreakpointValue,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure
} from '@chakra-ui/react';
import { InfoIcon } from '@chakra-ui/icons';
import HorizontalDynamicNewsEventCard from '../EventCard/HorizontalDynamicNewsEventCard';
import EventDetailScrollPanel from './EventDetailScrollPanel';
import DynamicNewsDetailPanel from '../DynamicNewsDetail/DynamicNewsDetailPanel';
import EventDetailModal from '../EventDetailModal';
/**
* 纵向分栏模式布局
@@ -165,20 +159,11 @@ const VerticalModeLayout = React.memo(({
{/* 移动端详情弹窗 */}
{isMobile && (
<Modal isOpen={isMobileModalOpen} onClose={onMobileModalClose} size="full" scrollBehavior="inside">
<ModalOverlay bg="blackAlpha.800" backdropFilter="blur(10px)" />
<ModalContent maxW="100vw" m={0} borderRadius={0}>
<ModalHeader bg="gray.900" color="white" borderBottom="1px solid" borderColor="gray.700">
{mobileSelectedEvent?.title || '事件详情'}
</ModalHeader>
<ModalCloseButton color="white" />
<ModalBody p={0} bg="gray.900">
{mobileSelectedEvent && (
<DynamicNewsDetailPanel event={mobileSelectedEvent} showHeader={false} />
)}
</ModalBody>
</ModalContent>
</Modal>
<EventDetailModal
open={isMobileModalOpen}
onClose={onMobileModalClose}
event={mobileSelectedEvent}
/>
)}
</Flex>
);

View File

@@ -47,7 +47,7 @@ const CompactMetaBar = ({ event, importance, isFollowing, followerCount, onToggl
spacing={3}
zIndex={1}
>
{/* 重要性徽章 - 与 EventHeaderInfo 样式一致,尺寸略小 */}
{/* 重要性徽章 - 与 EventHeaderInfo 样式一致,尺寸略小 - H5 隐藏 */}
<Badge
px={3}
py={1.5}
@@ -62,7 +62,7 @@ const CompactMetaBar = ({ event, importance, isFollowing, followerCount, onToggl
}
color="white"
boxShadow="lg"
display="flex"
display={{ base: 'none', lg: 'flex' }}
alignItems="center"
gap={1}
>

View File

@@ -2,13 +2,15 @@
// 精简模式股票卡片组件(浮动卡片样式)
import React from 'react';
import { useSelector } from 'react-redux';
import {
Box,
Text,
Tooltip,
useColorModeValue,
} from '@chakra-ui/react';
import { getChangeColor, getChangeBackgroundGradient, getChangeBorderColor } from '../../../../utils/colorUtils';
import { selectIsMobile } from '@store/slices/deviceSlice';
import { getChangeColor, getChangeBackgroundGradient, getChangeBorderColor } from '@utils/colorUtils';
/**
* 精简模式股票卡片组件
@@ -17,6 +19,7 @@ import { getChangeColor, getChangeBackgroundGradient, getChangeBorderColor } fro
* @param {Object} props.quote - 股票行情数据(可选)
*/
const CompactStockItem = ({ stock, quote = null }) => {
const isMobile = useSelector(selectIsMobile);
const nameColor = useColorModeValue('gray.700', 'gray.300');
const handleViewDetail = () => {
@@ -45,10 +48,10 @@ const CompactStockItem = ({ stock, quote = null }) => {
>
<Box
bgGradient={getChangeBackgroundGradient(change)}
borderWidth="3px"
borderWidth="1px"
borderColor={getChangeBorderColor(change)}
borderRadius="2xl"
p={4}
borderRadius="xl"
p={2}
onClick={handleViewDetail}
cursor="pointer"
boxShadow="lg"
@@ -69,14 +72,14 @@ const CompactStockItem = ({ stock, quote = null }) => {
}}
transition="all 0.3s ease-in-out"
display="inline-block"
minW="150px"
minW="100px"
>
{/* 股票代码 */}
<Text
fontSize="md"
fontSize={isMobile ? "sm" : "md"}
fontWeight="bold"
color={getChangeColor(change)}
mb={2}
mb={isMobile ? 1 : 2}
textAlign="center"
>
{stock.stock_code}
@@ -84,7 +87,7 @@ const CompactStockItem = ({ stock, quote = null }) => {
{/* 涨跌幅 - 超大号显示 */}
<Text
fontSize="3xl"
fontSize={isMobile ? "xl" : "3xl"}
fontWeight="black"
color={getChangeColor(change)}
textAlign="center"
@@ -96,9 +99,9 @@ const CompactStockItem = ({ stock, quote = null }) => {
{/* 股票名称(小字) */}
<Text
fontSize="xs"
fontSize={isMobile ? "2xs" : "xs"}
color={nameColor}
mt={2}
mt={isMobile ? 1 : 2}
textAlign="center"
noOfLines={1}
fontWeight="medium"

View File

@@ -418,7 +418,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
</CollapsibleSection>
{/* 讨论区(评论区) - 所有登录用户可用 */}
<Box mt={4}>
<Box>
<EventCommentSection eventId={event.id} />
</Box>
</VStack>

View File

@@ -37,16 +37,16 @@ const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => {
return (
<VStack
align="stretch"
spacing={2}
spacing={1}
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="md"
px={4}
py={2}
px={2}
py={1}
cursor="pointer"
transition="all 0.2s"
minW="200px"
minW="100px"
_hover={{
transform: 'translateY(-1px)',
boxShadow: 'md',
@@ -68,17 +68,17 @@ const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => {
</Text>
{/* 第二行:相关度 + 涨跌幅 */}
<Flex justify="space-between" align="center" gap={2} flexWrap="wrap">
<Flex justify="space-between" align="center" gap={1} flexWrap="wrap">
{/* 相关度标签 */}
<Box
bg={relevanceColors.bg}
color={relevanceColors.color}
px={2}
px={1.5}
py={0.5}
borderRadius="sm"
flexShrink={0}
>
<Text fontSize="xs" fontWeight="medium" whiteSpace="nowrap">
<Text fontSize="10px" fontWeight="medium" whiteSpace="nowrap">
相关度: {relevanceScore}%
</Text>
</Box>
@@ -87,8 +87,8 @@ const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => {
{changePct !== null && (
<Badge
colorScheme={changeColor}
fontSize="xs"
px={2}
fontSize="10px"
px={1.5}
py={0.5}
flexShrink={0}
>

View File

@@ -243,7 +243,7 @@ const RelatedConceptsSection = ({
}
}}
>
{isExpanded ? '收起' : '查看详细描述'}
{isExpanded ? '收起' : '查看详细'}
</Button>
</Flex>
{/* 第二行:交易日期信息 */}

View File

@@ -2,6 +2,7 @@
// 股票卡片组件(融合表格功能的卡片样式)
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import {
Box,
Flex,
@@ -16,13 +17,15 @@ import {
useColorModeValue,
} from '@chakra-ui/react';
import { StarIcon } from '@chakra-ui/icons';
import { Tag } from 'antd';
import { RobotOutlined } from '@ant-design/icons';
import { selectIsMobile } from '@store/slices/deviceSlice';
import MiniTimelineChart from '../StockDetailPanel/components/MiniTimelineChart';
import MiniKLineChart from './MiniKLineChart';
import TimelineChartModal from '../../../../components/StockChart/TimelineChartModal';
import KLineChartModal from '../../../../components/StockChart/KLineChartModal';
import CitedContent from '../../../../components/Citation/CitedContent';
import { getChangeColor } from '../../../../utils/colorUtils';
import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme';
import TimelineChartModal from '@components/StockChart/TimelineChartModal';
import KLineChartModal from '@components/StockChart/KLineChartModal';
import { getChangeColor } from '@utils/colorUtils';
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
/**
* 股票卡片组件
@@ -44,6 +47,7 @@ const StockListItem = ({
isInWatchlist = false,
onWatchlistToggle
}) => {
const isMobile = useSelector(selectIsMobile);
const cardBg = PROFESSIONAL_COLORS.background.card;
const borderColor = PROFESSIONAL_COLORS.border.default;
const codeColor = '#3B82F6';
@@ -128,9 +132,9 @@ const StockListItem = ({
transition="all 0.2s"
>
{/* 单行紧凑布局:名称+涨跌幅 | 分时图 | K线图 | 关联描述 */}
<HStack spacing={2} align="center" flexWrap="wrap">
<HStack spacing={2} align="center" flexWrap={isMobile ? 'wrap' : 'nowrap'}>
{/* 左侧:股票信息区 */}
<HStack spacing={2} minW="360px" maxW="380px" flexShrink={0}>
<HStack spacing={2} overflow="hidden">
{/* 股票代码 + 名称 + 涨跌幅 */}
<VStack
align="stretch"
@@ -194,24 +198,24 @@ const StockListItem = ({
</HStack>
</VStack>
{/* 分时图 - 更紧凑 */}
{/* 分时图 - 自适应 */}
<VStack
w="115px"
flex={1}
minW="80px"
maxW="150px"
borderWidth="1px"
borderColor="rgba(59, 130, 246, 0.3)"
borderRadius="md"
px={1.5}
py={1}
px={2}
py={1.5}
bg="rgba(59, 130, 246, 0.1)"
onClick={(e) => {
e.stopPropagation();
setIsTimelineModalOpen(true);
}}
cursor="pointer"
flexShrink={0}
align="stretch"
spacing={0}
h="fit-content"
_hover={{
borderColor: '#3B82F6',
boxShadow: '0 0 10px rgba(59, 130, 246, 0.3)',
@@ -228,7 +232,7 @@ const StockListItem = ({
>
📈 分时
</Text>
<Box h="32px">
<Box h="28px">
<MiniTimelineChart
stockCode={stock.stock_code}
eventTime={eventTime}
@@ -236,24 +240,24 @@ const StockListItem = ({
</Box>
</VStack>
{/* K线图 - 更紧凑 */}
{/* K线图 - 自适应 */}
<VStack
w="115px"
flex={1}
minW="80px"
maxW="150px"
borderWidth="1px"
borderColor="rgba(168, 85, 247, 0.3)"
borderRadius="md"
px={1.5}
py={1}
px={2}
py={1.5}
bg="rgba(168, 85, 247, 0.1)"
onClick={(e) => {
e.stopPropagation();
setIsKLineModalOpen(true);
}}
cursor="pointer"
flexShrink={0}
align="stretch"
spacing={0}
h="fit-content"
_hover={{
borderColor: '#A855F7',
boxShadow: '0 0 10px rgba(168, 85, 247, 0.3)',
@@ -270,7 +274,7 @@ const StockListItem = ({
>
📊 日线
</Text>
<Box h="32px">
<Box h="28px">
<MiniKLineChart
stockCode={stock.stock_code}
eventTime={eventTime}
@@ -281,7 +285,7 @@ const StockListItem = ({
{/* 关联描述 - 升级和降级处理 */}
{stock.relation_desc && (
<Box flex={1} minW={0}>
<Box flex={1} minW={0} flexBasis={isMobile ? '100%' : ''}>
{stock.relation_desc?.data ? (
// 升级:带引用来源的版本 - 添加折叠功能
<Tooltip
@@ -298,8 +302,6 @@ const StockListItem = ({
setIsDescExpanded(!isDescExpanded);
}}
cursor="pointer"
px={3}
py={2}
bg={PROFESSIONAL_COLORS.background.secondary}
borderRadius="md"
_hover={{
@@ -308,18 +310,30 @@ const StockListItem = ({
transition="background 0.2s"
position="relative"
>
<Collapse in={isDescExpanded} startingHeight={40}>
<CitedContent
data={stock.relation_desc}
title=""
showAIBadge={true}
textColor={PROFESSIONAL_COLORS.text.primary}
containerStyle={{
backgroundColor: 'transparent',
borderRadius: '0',
padding: '0',
<Collapse in={isDescExpanded} startingHeight={56}>
{/* AI 标识 - 行内显示在文字前面 */}
<Tag
icon={<RobotOutlined />}
color="purple"
style={{
fontSize: 12,
padding: '2px 8px',
marginRight: 8,
verticalAlign: 'middle',
display: 'inline-flex',
}}
/>
>
AI合成
</Tag>
{/* 直接渲染文字内容 */}
<Text
as="span"
fontSize="sm"
color={PROFESSIONAL_COLORS.text.primary}
lineHeight="1.8"
>
{stock.relation_desc?.data?.map(item => item.sentences || item.query_part).filter(Boolean).join('')}
</Text>
</Collapse>
</Box>
</Tooltip>
@@ -339,8 +353,6 @@ const StockListItem = ({
setIsDescExpanded(!isDescExpanded);
}}
cursor="pointer"
px={3}
py={2}
bg={PROFESSIONAL_COLORS.background.secondary}
borderRadius="md"
_hover={{
@@ -350,7 +362,7 @@ const StockListItem = ({
position="relative"
>
{/* 去掉"关联描述"标题 */}
<Collapse in={isDescExpanded} startingHeight={36}>
<Collapse in={isDescExpanded} startingHeight={56}>
<Text
fontSize="xs"
color={nameColor}

View File

@@ -32,10 +32,10 @@ const EventFollowButton = ({
size={size}
colorScheme="yellow"
variant="ghost"
bg="whiteAlpha.500"
bg="rgba(113, 128, 150, 0.6)"
boxShadow="sm"
_hover={{
bg: 'whiteAlpha.800',
bg: 'rgba(113, 128, 150, 0.8)',
boxShadow: 'md'
}}
icon={
@@ -47,8 +47,7 @@ const EventFollowButton = ({
) : (
<AiOutlineStar
size={iconSize}
color="#718096"
strokeWidth="1"
color="gold"
/>
)
}

View File

@@ -15,6 +15,8 @@ import {
} from '@chakra-ui/react';
import { getImportanceConfig } from '../../../../constants/importanceLevels';
import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme';
import { useDevice } from '@hooks/useDevice';
import dayjs from 'dayjs';
// 导入子组件
import ImportanceStamp from './ImportanceStamp';
@@ -54,6 +56,7 @@ const HorizontalDynamicNewsEventCard = React.memo(({
layout = 'vertical',
}) => {
const importance = getImportanceConfig(event.importance);
const { isMobile } = useDevice();
// 专业配色 - 黑色、灰色、金色主题
const cardBg = PROFESSIONAL_COLORS.background.card;
@@ -67,8 +70,8 @@ const HorizontalDynamicNewsEventCard = React.memo(({
const showTimeline = useBreakpointValue({ base: false, md: true }); // 移动端隐藏时间轴
const cardPadding = useBreakpointValue({ base: 2, md: 3 }); // 移动端减小内边距
const titleFontSize = useBreakpointValue({ base: 'sm', md: 'md' }); // 移动端减小标题字体
const titlePaddingRight = useBreakpointValue({ base: '80px', md: '120px' }); // 为关键词留空间
const spacing = useBreakpointValue({ base: 2, md: 3 }); // 间距
const titlePaddingRight = useBreakpointValue({ base: '16px', md: '120px' }); // 桌面端为关键词留空间,移动端不显示关键词
const spacing = useBreakpointValue({ base: 1, md: 3 }); // 间距(移动端更紧凑)
/**
* 根据平均涨幅计算背景色(专业配色 - 深色主题)
@@ -159,6 +162,33 @@ const HorizontalDynamicNewsEventCard = React.memo(({
onClick={() => onEventClick?.(event)}
>
<CardBody p={cardPadding} pb={2}>
{/* 左上角:移动端时间显示 */}
{isMobile && (
<Box
position="absolute"
top={1}
left={1}
zIndex={2}
{...(timelineStyle.bgGradient ? { bgGradient: timelineStyle.bgGradient } : { bg: timelineStyle.bg })}
borderWidth={timelineStyle.borderWidth}
borderColor={timelineStyle.borderColor}
borderRadius="md"
px={1.5}
py={1}
textAlign="center"
boxShadow={timelineStyle.boxShadow}
>
<Text
fontSize="9px"
fontWeight="bold"
color={timelineStyle.textColor}
lineHeight="1.2"
>
{dayjs(event.created_at).format('MM-DD HH:mm')}
</Text>
</Box>
)}
{/* 右上角:关注按钮 */}
<Box position="absolute" top={{ base: 1, md: 2 }} right={{ base: 1, md: 2 }} zIndex={2}>
<EventFollowButton
@@ -170,8 +200,8 @@ const HorizontalDynamicNewsEventCard = React.memo(({
/>
</Box>
{/* Keywords梦幻轮播 - 绝对定位在卡片右侧空白处 */}
{event.keywords && event.keywords.length > 0 && (
{/* Keywords梦幻轮播 - 绝对定位在卡片右侧空白处(移动端隐藏) */}
{!isMobile && event.keywords && event.keywords.length > 0 && (
<KeywordsCarousel
keywords={event.keywords}
interval={4000}
@@ -200,6 +230,7 @@ const HorizontalDynamicNewsEventCard = React.memo(({
onClick={(e) => onTitleClick?.(e, event)}
mt={1}
paddingRight={titlePaddingRight}
paddingLeft={isMobile ? '70px' : undefined}
>
<Text
fontSize={titleFontSize}

View File

@@ -0,0 +1,36 @@
.event-detail-modal {
top: 20% !important;
margin: 0 auto !important;
padding-bottom: 0 !important;
.ant-modal-content {
border-radius: 24px !important;
background: transparent;
}
// 标题样式 - 深色文字(白色背景)
.ant-modal-title {
color: #1A202C;
}
// 关闭按钮样式 - 深色(白色背景)
.ant-modal-close {
color: #4A5568;
&:hover {
color: #1A202C;
}
}
}
// 自底向上滑入动画
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Modal } from 'antd';
import { selectIsMobile } from '@store/slices/deviceSlice';
import DynamicNewsDetailPanel from './DynamicNewsDetail/DynamicNewsDetailPanel';
import './EventDetailModal.less';
interface EventDetailModalProps {
/** 是否打开弹窗 */
open: boolean;
/** 关闭弹窗回调 */
onClose: () => void;
/** 事件对象 */
event: any; // TODO: 后续可替换为具体的 Event 类型
}
/**
* 事件详情弹窗组件
*/
const EventDetailModal: React.FC<EventDetailModalProps> = ({
open,
onClose,
event,
}) => {
const isMobile = useSelector(selectIsMobile);
return (
<Modal
open={open}
onCancel={onClose}
footer={null}
title={event?.title || '事件详情'}
width='100vw'
destroyOnClose
className="event-detail-modal"
styles={{
mask: { background: 'transparent' },
content: { borderRadius: 24, padding: 0, maxWidth: 1400, background: 'transparent', margin: '0 auto' },
header: { background: '#FFFFFF', borderBottom: '1px solid #E2E8F0', padding: '16px 24px', borderRadius: '24px 24px 0 0', margin: 0 },
body: { padding: 0 },
}}
>
{event && <DynamicNewsDetailPanel event={event} showHeader={false} />}
</Modal>
);
};
export default EventDetailModal;

View File

@@ -37,7 +37,7 @@
}
.hot-events-carousel {
padding: 0 40px; /* 增加左右padding箭头留出空间 */
padding: 0; /* 移除左右padding箭头使用绝对定位 */
position: relative;
}
@@ -65,13 +65,20 @@
color: #096dd9 !important;
}
/* 箭头位置 */
/* 箭头位置 - 绝对定位,悬浮在卡片边缘 */
.hot-events-carousel .slick-prev.custom-carousel-arrow {
left: 0 !important;
left: 8px !important;
position: absolute;
}
.hot-events-carousel .slick-next.custom-carousel-arrow {
right: 0 !important;
right: 8px !important;
position: absolute;
}
/* 隐藏可能重复的默认箭头 */
.hot-events-carousel .slick-arrow:not(.custom-carousel-arrow) {
display: none !important;
}
/* 禁用状态 */

View File

@@ -2,19 +2,11 @@
import React, { useState } from 'react';
import { Card, Badge, Tag, Empty, Carousel, Tooltip } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure
} from '@chakra-ui/react';
import { useDisclosure } from '@chakra-ui/react';
import EventDetailModal from './EventDetailModal';
import dayjs from 'dayjs';
import './HotEvents.css';
import defaultEventImage from '../../../assets/img/default-event.jpg';
import DynamicNewsDetailPanel from './DynamicNewsDetail';
// 自定义箭头组件
const CustomArrow = ({ className, style, onClick, direction }) => {
@@ -196,21 +188,12 @@ const HotEvents = ({ events, onPageChange, onEventClick }) => {
</Card>
)}
{/* 事件详情弹窗 - 使用 Chakra UI Modal与平铺模式一致 */}
{isModalOpen ? (
<Modal isOpen={isModalOpen} onClose={onModalClose} size="6xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent>
<ModalHeader>
{modalEvent?.title || '事件详情'}
</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
{modalEvent && <DynamicNewsDetailPanel event={modalEvent} />}
</ModalBody>
</ModalContent>
</Modal>
): null}
{/* 事件详情弹窗 */}
<EventDetailModal
open={isModalOpen}
onClose={onModalClose}
event={modalEvent}
/>
</div>
);
};

View File

@@ -1,20 +1,25 @@
// src/views/Community/components/TradingTimeFilter.js
// 交易时段智能筛选组件
import React, { useState, useMemo, useEffect } from 'react';
import { Space, Button, Tag, Tooltip, DatePicker, Popover } from 'antd';
import { ClockCircleOutlined, CalendarOutlined } from '@ant-design/icons';
import { Space, Button, Tag, Tooltip, DatePicker, Popover, Select } from 'antd';
import { ClockCircleOutlined, CalendarOutlined, FilterOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import locale from 'antd/es/date-picker/locale/zh_CN';
import { logger } from '../../../utils/logger';
import { logger } from '@utils/logger';
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
const { RangePicker } = DatePicker;
const { Option } = Select;
/**
* 交易时段筛选组件
* @param {string} value - 当前选中的 key受控
* @param {Function} onChange - 时间范围变化回调 (timeConfig) => void
* @param {boolean} compact - 是否使用紧凑模式PC 端搜索栏内使用)
* @param {boolean} mobile - 是否使用移动端模式(下拉选择)
*/
const TradingTimeFilter = ({ value, onChange }) => {
const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false }) => {
const [selectedKey, setSelectedKey] = useState(null);
const [customRangeVisible, setCustomRangeVisible] = useState(false);
const [customRange, setCustomRange] = useState(null);
@@ -266,7 +271,39 @@ const TradingTimeFilter = ({ value, onChange }) => {
}
};
// 渲染按钮
// 渲染紧凑模式按钮PC 端搜索栏内使用,文字按钮 + | 分隔符)
const renderCompactButton = (config, showDivider = true) => {
const isSelected = selectedKey === config.key;
const fullTooltip = config.timeHint ? `${config.tooltip} · ${config.timeHint}` : config.tooltip;
return (
<React.Fragment key={config.key}>
<Tooltip title={fullTooltip}>
<span
onClick={() => handleButtonClick(config)}
style={{
cursor: 'pointer',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '13px',
fontWeight: isSelected ? 600 : 400,
color: isSelected ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.text.secondary,
background: isSelected ? 'rgba(255, 195, 0, 0.15)' : 'transparent',
transition: 'all 0.2s ease',
whiteSpace: 'nowrap',
}}
>
{config.label}
</span>
</Tooltip>
{showDivider && (
<span style={{ color: 'rgba(255, 255, 255, 0.2)', margin: '0 2px' }}>|</span>
)}
</React.Fragment>
);
};
// 渲染按钮(默认模式)
const renderButton = (config) => {
const isSelected = selectedKey === config.key;
@@ -321,6 +358,98 @@ const TradingTimeFilter = ({ value, onChange }) => {
</div>
);
// 移动端模式:下拉选择器
if (mobile) {
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
const handleMobileSelect = (key) => {
if (key === selectedKey) {
// 取消选中
setSelectedKey(null);
onChange(null);
} else {
const config = allButtons.find(b => b.key === key);
if (config) {
handleButtonClick(config);
}
}
};
return (
<Select
value={selectedKey}
onChange={handleMobileSelect}
placeholder={
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<FilterOutlined style={{ fontSize: 12 }} />
筛选
</span>
}
allowClear
onClear={() => {
setSelectedKey(null);
onChange(null);
}}
style={{ minWidth: 80 }}
className="transparent-select"
popupMatchSelectWidth={false}
>
{allButtons.map(config => (
<Option key={config.key} value={config.key}>
{config.label}
</Option>
))}
</Select>
);
}
// 紧凑模式PC 端搜索栏内的样式
if (compact) {
// 合并所有按钮配置
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
return (
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'nowrap' }}>
{allButtons.map((config, index) =>
renderCompactButton(config, index < allButtons.length - 1)
)}
{/* 更多时间 */}
<span style={{ color: 'rgba(255, 255, 255, 0.2)', margin: '0 2px' }}>|</span>
<Popover
content={customRangeContent}
title="选择自定义时间范围"
trigger="click"
open={customRangeVisible}
onOpenChange={setCustomRangeVisible}
placement="bottomLeft"
>
<Tooltip title="自定义时间范围">
<span
style={{
cursor: 'pointer',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '13px',
fontWeight: selectedKey === 'custom' ? 600 : 400,
color: selectedKey === 'custom' ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.text.secondary,
background: selectedKey === 'custom' ? 'rgba(255, 195, 0, 0.15)' : 'transparent',
transition: 'all 0.2s ease',
whiteSpace: 'nowrap',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
<CalendarOutlined style={{ fontSize: 12 }} />
更多
</span>
</Tooltip>
</Popover>
</div>
);
}
// 默认模式:移动端/独立使用
return (
<Space wrap size={[8, 8]} style={{ display: 'flex', alignItems: 'flex-start' }}>
{/* 动态按钮(根据时段显示多个) */}

View File

@@ -1,5 +1,5 @@
// src/views/Community/index.js
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, lazy, Suspense } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import {
@@ -10,22 +10,15 @@ import {
Box,
Container,
useColorModeValue,
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
Button,
CloseButton,
HStack,
VStack,
Text,
useBreakpointValue,
Skeleton,
} from '@chakra-ui/react';
// 导入组件
import DynamicNewsCard from './components/DynamicNewsCard';
import HotEventsSection from './components/HotEventsSection';
import HeroPanel from './components/HeroPanel';
// ⚡ HeroPanel 懒加载:包含 ECharts (~600KB),首屏不需要立即渲染
const HeroPanel = lazy(() => import('./components/HeroPanel'));
// 导入自定义 Hooks
import { useEventData } from './hooks/useEventData';
@@ -49,8 +42,6 @@ const Community = () => {
// 专业配色 - 深色主题
const bgColor = PROFESSIONAL_COLORS.background.primary;
const alertBgColor = 'rgba(59, 130, 246, 0.1)';
const alertBorderColor = PROFESSIONAL_COLORS.border.default;
// Ref用于首次滚动到内容区域
const containerRef = useRef(null);
@@ -78,9 +69,6 @@ const Community = () => {
// ⚡ DynamicNewsCard 的 ref用于触发刷新
const dynamicNewsCardRef = useRef(null);
// 通知横幅显示状态
const [showNotificationBanner, setShowNotificationBanner] = useState(false);
// 🎯 初始化Community埋点Hook
const communityEvents = useCommunityEvents({ navigate });
@@ -119,39 +107,6 @@ const Community = () => {
}
}, [events, loading, pagination, filters]);
// ⚡ 检查通知权限状态,显示横幅提示
useEffect(() => {
// 延迟3秒显示让用户先浏览页面
const timer = setTimeout(() => {
// 如果未授权或未请求过权限,显示横幅
if (browserPermission !== 'granted') {
const hasClosedBanner = localStorage.getItem('notification_banner_closed');
if (!hasClosedBanner) {
setShowNotificationBanner(true);
logger.info('Community', '显示通知权限横幅');
}
}
}, 3000);
return () => clearTimeout(timer);
}, [browserPermission]);
// 处理开启通知
const handleEnableNotifications = async () => {
const permission = await requestBrowserPermission();
if (permission === 'granted') {
setShowNotificationBanner(false);
logger.info('Community', '通知权限已授予');
}
};
// 处理关闭横幅
const handleCloseBanner = () => {
setShowNotificationBanner(false);
localStorage.setItem('notification_banner_closed', 'true');
logger.info('Community', '通知横幅已关闭');
};
// ⚡ 首次进入页面时滚动到内容区域(考虑导航栏高度)
const hasScrolled = useRef(false);
useEffect(() => {
@@ -235,45 +190,14 @@ const Community = () => {
<Box minH="100vh" bg={bgColor}>
{/* 主内容区域 */}
<Container ref={containerRef} maxW={containerMaxW} px={containerPx} pt={{ base: 3, md: 6 }} pb={{ base: 4, md: 8 }}>
{/* 通知权限提示横幅 */}
{showNotificationBanner && (
<Alert
status="info"
variant="subtle"
borderRadius="lg"
mb={4}
boxShadow="md"
bg={alertBgColor}
borderWidth="1px"
borderColor={alertBorderColor}
>
<AlertIcon />
<Box flex="1">
<AlertTitle fontSize="md" mb={1}>
开启桌面通知不错过重要事件
</AlertTitle>
<AlertDescription fontSize="sm">
即使浏览器最小化也能第一时间接收新事件推送通知
</AlertDescription>
</Box>
<HStack spacing={2} ml={4}>
<Button
size="sm"
colorScheme="blue"
onClick={handleEnableNotifications}
>
立即开启
</Button>
<CloseButton
onClick={handleCloseBanner}
position="relative"
/>
</HStack>
</Alert>
)}
{/* 顶部说明面板:产品介绍 + 沪深指数 + 热门概念词云 */}
<HeroPanel />
{/* ⚡ 顶部说明面板(懒加载):产品介绍 + 沪深指数 + 热门概念词云 */}
<Suspense fallback={
<Box mb={6} p={4} borderRadius="xl" bg="rgba(255,255,255,0.02)">
<Skeleton height="200px" borderRadius="lg" startColor="gray.800" endColor="gray.700" />
</Box>
}>
<HeroPanel />
</Suspense>
{/* 实时要闻·动态追踪 - 横向滚动 */}
<DynamicNewsCard

View File

@@ -1358,17 +1358,8 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
</MarkdownRenderer>
</Box>
)}
{analysis.news_summary && (
<Box p={4} bg={colorMode === 'light' ? 'green.50' : 'green.900'} borderRadius="md">
<Heading size="sm" mb={2} color={theme.primary}>相关新闻</Heading>
<MarkdownRenderer theme={theme} colorMode={colorMode}>
{analysis.news_summary}
</MarkdownRenderer>
</Box>
)}
{analysis.announcements && (
{analysis.announcements && analysis.announcements !== '[]' && (
<Box p={4} bg={colorMode === 'light' ? 'orange.50' : 'orange.900'} borderRadius="md">
<Heading size="sm" mb={2} color={theme.primary}>相关公告</Heading>
<MarkdownRenderer theme={theme} colorMode={colorMode}>
@@ -1376,21 +1367,93 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
</MarkdownRenderer>
</Box>
)}
{analysis.guba_sentiment && (
<Box p={4} bg={colorMode === 'light' ? 'red.50' : 'red.900'} borderRadius="md">
<Heading size="sm" mb={2} color={theme.primary}>股吧情绪</Heading>
<MarkdownRenderer theme={theme} colorMode={colorMode}>
{analysis.guba_sentiment}
</MarkdownRenderer>
{/* 研报引用展示 */}
{analysis.verification_reports && analysis.verification_reports.length > 0 && (
<Box p={4} bg={colorMode === 'light' ? 'blue.50' : 'blue.900'} borderRadius="md">
<Heading size="sm" mb={3} color={theme.primary}>
<HStack spacing={2}>
<Icon as={ExternalLinkIcon} />
<Text>研报引用 ({analysis.verification_reports.length})</Text>
</HStack>
</Heading>
<VStack spacing={3} align="stretch">
{analysis.verification_reports.map((report, reportIdx) => (
<Box
key={reportIdx}
p={3}
bg={colorMode === 'light' ? 'white' : 'gray.800'}
borderRadius="md"
border="1px solid"
borderColor={theme.border}
>
<HStack justify="space-between" mb={2}>
<HStack spacing={2}>
<Badge colorScheme="blue" fontSize="xs">
{report.publisher || '未知机构'}
</Badge>
{report.match_score && (
<Badge
colorScheme={report.match_score === '好' ? 'green' : report.match_score === '中' ? 'yellow' : 'gray'}
fontSize="xs"
>
匹配度: {report.match_score}
</Badge>
)}
{report.match_ratio != null && report.match_ratio > 0 && (
<Badge colorScheme="purple" fontSize="xs">
{(report.match_ratio * 100).toFixed(0)}%
</Badge>
)}
</HStack>
{report.declare_date && (
<Text fontSize="xs" color={theme.textMuted}>
{report.declare_date.substring(0, 10)}
</Text>
)}
</HStack>
{report.report_title && (
<Text fontWeight="bold" fontSize="sm" color={theme.textPrimary} mb={1}>
{report.report_title}
</Text>
)}
{report.author && (
<Text fontSize="xs" color={theme.textMuted} mb={2}>
分析师: {report.author}
</Text>
)}
{report.verification_item && (
<Box
p={2}
bg={colorMode === 'light' ? 'yellow.50' : 'yellow.900'}
borderRadius="sm"
mb={2}
>
<Text fontSize="xs" color={theme.textMuted}>
<strong>验证项:</strong> {report.verification_item}
</Text>
</Box>
)}
{report.content && (
<Text fontSize="sm" color={theme.textSecondary} noOfLines={4}>
{report.content}
</Text>
)}
</Box>
))}
</VStack>
</Box>
)}
<Box mt={4}>
<Text fontSize="sm" color={theme.textMuted}>
成交量: {formatUtils.formatNumber(analysis.volume)} |
成交额: {formatUtils.formatNumber(analysis.amount)} |
分析时间: {analysis.analysis_time || '-'}
成交量: {formatUtils.formatNumber(analysis.volume)} |
成交额: {formatUtils.formatNumber(analysis.amount)} |
更新时间: {analysis.update_time || analysis.create_time || '-'}
</Text>
</Box>
</VStack>

View File

@@ -44,8 +44,8 @@ import {
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import { DateClickArg } from '@fullcalendar/interaction';
import { EventClickArg } from '@fullcalendar/common';
import type { DateClickArg } from '@fullcalendar/interaction';
import type { EventClickArg } from '@fullcalendar/core';
import dayjs, { Dayjs } from 'dayjs';
import 'dayjs/locale/zh-cn';

View File

@@ -247,7 +247,7 @@ export const PlansPanel: React.FC = () => {
};
// 渲染单个卡片
const renderCard = (item: InvestmentEvent): JSX.Element => {
const renderCard = (item: InvestmentEvent): React.ReactElement => {
const statusInfo = getStatusInfo(item.status);
return (

View File

@@ -247,7 +247,7 @@ export const ReviewsPanel: React.FC = () => {
};
// 渲染单个卡片
const renderCard = (item: InvestmentEvent): JSX.Element => {
const renderCard = (item: InvestmentEvent): React.ReactElement => {
const statusInfo = getStatusInfo(item.status);
return (

View File

@@ -78,14 +78,13 @@ const KLineChartView: React.FC<KLineChartViewProps> = ({
},
},
candle: {
type: 'line', // 使用折线图模式
line: {
upColor: themeColors.primary.gold,
downColor: themeColors.primary.gold,
style: 'solid',
size: 2,
type: 'area' as const, // 使用面积图模式
area: {
lineColor: themeColors.primary.gold,
lineSize: 2,
backgroundColor: [`${themeColors.primary.gold}30`, `${themeColors.primary.gold}05`],
},
},
} as any,
crosshair: {
horizontal: {
line: {
@@ -148,7 +147,7 @@ const KLineChartView: React.FC<KLineChartViewProps> = ({
.sort((a, b) => a.timestamp - b.timestamp);
// 设置数据
chart?.applyNewData(chartData);
(chart as any)?.applyNewData(chartData);
chartRef.current = chart;

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