Compare commits
39 Commits
before-rec
...
e277352133
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e277352133 | ||
|
|
87437ed229 | ||
|
|
037471d880 | ||
|
|
0c482bc72c | ||
|
|
4aebb3bf4b | ||
|
|
ed241bd9c5 | ||
|
|
e6ede81c78 | ||
|
|
a0b688da80 | ||
|
|
6bd09b797d | ||
|
|
9c532b5f18 | ||
|
|
1d1d6c8169 | ||
|
|
3507cfe9f7 | ||
|
|
cc520893f8 | ||
|
|
dabedc1c0b | ||
|
|
7b4c4be7bf | ||
|
|
7a2c73f3ca | ||
|
|
105a0b02ea | ||
|
|
d8a4c20565 | ||
|
|
5f959fb44f | ||
|
|
ee78e00d3b | ||
|
|
2fcc341213 | ||
|
|
1090a2fc67 | ||
|
|
77f3949fe2 | ||
|
|
742ab337dc | ||
|
|
d2b6904a4a | ||
|
|
789a6229a7 | ||
|
|
6886a649f5 | ||
|
|
581e874b0d | ||
|
|
b23ed93020 | ||
|
|
84f70f3329 | ||
|
|
601b06d79e | ||
|
|
0818a7bff7 | ||
| ce19881181 | |||
| bef3e86f60 | |||
| 65deea43e2 | |||
| c7a881c965 | |||
| 6932796b00 | |||
|
|
03f1331202 | ||
|
|
c771f7cae6 |
221
app.py
221
app.py
@@ -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_login 获取用户信息)
|
||||
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_login&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
332
app_vx.py
@@ -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
553
public/htmls/TPU芯片.html
Normal 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
876
public/htmls/海军.html
Normal 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>
|
||||
648
public/htmls/谷歌概念.html
Normal file
648
public/htmls/谷歌概念.html
Normal 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>
|
||||
824
public/htmls/远程火力.html
Normal file
824
public/htmls/远程火力.html
Normal 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>
|
||||
643
public/htmls/阿里AI千问、灵光.html
Normal file
643
public/htmls/阿里AI千问、灵光.html
Normal 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编程能力达全球SOTA,API调用量突破千亿级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>
|
||||
@@ -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
|
||||
145
src/App.js
145
src/App.js
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
/**
|
||||
* 组件卸载时清理定时器和标记组件状态
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 world’s 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;
|
||||
@@ -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 world’s 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
// 忽略清理时的错误
|
||||
|
||||
@@ -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', '调整图表大小');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* 创建事件标记 Overlay(KLineChart 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 });
|
||||
// 忽略高亮标记时的错误
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
66
src/constants/homeFeatures.ts
Normal file
66
src/constants/homeFeatures.ts
Normal 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: '实战'
|
||||
}
|
||||
];
|
||||
@@ -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);
|
||||
|
||||
@@ -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,42 @@ export const useNotification = () => {
|
||||
|
||||
// 通知提供者组件
|
||||
export const NotificationProvider = ({ children }) => {
|
||||
// ⚡ 移动端检测(使用 Redux 状态)
|
||||
const isMobile = useSelector(selectIsMobile);
|
||||
|
||||
// ⚡ 移动端禁用完整通知能力:返回空壳 Provider
|
||||
// 移动端不支持桌面通知,且不需要 Socket 实时推送
|
||||
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 toast = useToast();
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
@@ -649,183 +690,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);
|
||||
|
||||
@@ -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
35
src/hooks/useDevice.js
Normal 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;
|
||||
57
src/hooks/useHomeResponsive.ts
Normal file
57
src/hooks/useHomeResponsive.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
382
src/hooks/useSubscriptionEvents.ts
Normal file
382
src/hooks/useSubscriptionEvents.ts
Normal 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;
|
||||
46
src/index.js
46
src/index.js
@@ -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';
|
||||
@@ -115,19 +119,13 @@ function registerServiceWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
// 启动 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();
|
||||
}
|
||||
|
||||
// 启动应用(MSW 异步初始化,不阻塞首屏渲染)
|
||||
function startApp() {
|
||||
// Create root
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
|
||||
// Render the app with Router wrapper
|
||||
// ✅ StrictMode 已启用(Chakra UI 2.10.9+ 已修复兼容性问题)
|
||||
// ✅ 先渲染应用,不等待 MSW
|
||||
// StrictMode 已启用(Chakra UI 2.10.9+ 已修复兼容性问题)
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Router
|
||||
@@ -141,6 +139,30 @@ async function startApp() {
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// ✅ 后台异步启动 MSW(不阻塞首屏渲染)
|
||||
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_ENABLE_MOCK === 'true') {
|
||||
const initMSW = async () => {
|
||||
try {
|
||||
const { startMockServiceWorker } = await import('./mocks/browser');
|
||||
await startMockServiceWorker();
|
||||
console.log(
|
||||
'%c[MSW] ✅ Mock Service Worker 已在后台启动',
|
||||
'color: #4CAF50; font-weight: bold;'
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[MSW] ❌ Mock Service Worker 启动失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 使用 requestIdleCallback 在浏览器空闲时初始化,不阻塞首屏
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => initMSW(), { timeout: 3000 });
|
||||
} else {
|
||||
// 降级:使用 setTimeout(0) 延迟执行
|
||||
setTimeout(initMSW, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 Service Worker
|
||||
registerServiceWorker();
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. 检查 Session(AuthContext 使用的正确端点)
|
||||
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');
|
||||
}
|
||||
|
||||
22
src/mocks/handlers/bytedesk.js
Normal file
22
src/mocks/handlers/bytedesk.js
Normal 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;
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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() - 查看所有模拟订单');
|
||||
}
|
||||
|
||||
@@ -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')),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -329,10 +329,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/* Tailwind CSS 入口文件 */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 自定义工具类 */
|
||||
@layer utilities {
|
||||
/* 毛玻璃效果 */
|
||||
.backdrop-blur-xl {
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
}
|
||||
33
src/types/home.ts
Normal file
33
src/types/home.ts
Normal 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
44
src/types/static-assets.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/views/Community/index.js
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState, lazy, Suspense } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
@@ -20,12 +20,14 @@ import {
|
||||
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';
|
||||
@@ -272,8 +274,14 @@ const Community = () => {
|
||||
</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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,508 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
Icon,
|
||||
useColorMode,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { createChart, LineSeries } from 'lightweight-charts';
|
||||
import type { IChartApi, ISeriesApi, LineData, Time } from 'lightweight-charts';
|
||||
import {
|
||||
FaExpand,
|
||||
FaCompress,
|
||||
FaCamera,
|
||||
FaRedo,
|
||||
FaCog,
|
||||
} from 'react-icons/fa';
|
||||
import { MetricDataPoint } from '@services/categoryService';
|
||||
|
||||
// 黑金主题配色
|
||||
const themeColors = {
|
||||
bg: {
|
||||
primary: '#0a0a0a',
|
||||
secondary: '#1a1a1a',
|
||||
card: '#1e1e1e',
|
||||
},
|
||||
text: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#b8b8b8',
|
||||
muted: '#808080',
|
||||
gold: '#D4AF37',
|
||||
},
|
||||
border: {
|
||||
default: 'rgba(255, 255, 255, 0.1)',
|
||||
gold: 'rgba(212, 175, 55, 0.3)',
|
||||
},
|
||||
primary: {
|
||||
gold: '#D4AF37',
|
||||
goldLight: '#F4E3A7',
|
||||
},
|
||||
};
|
||||
|
||||
interface TradingViewChartProps {
|
||||
data: MetricDataPoint[];
|
||||
metricName: string;
|
||||
unit: string;
|
||||
frequency: string;
|
||||
}
|
||||
|
||||
type TimeRange = '1M' | '3M' | '6M' | '1Y' | 'YTD' | 'ALL';
|
||||
|
||||
const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
||||
data,
|
||||
metricName,
|
||||
unit,
|
||||
frequency,
|
||||
}) => {
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||
const chartRef = useRef<IChartApi | null>(null);
|
||||
const lineSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [selectedRange, setSelectedRange] = useState<TimeRange>('ALL');
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
// 初始化图表
|
||||
useEffect(() => {
|
||||
if (!chartContainerRef.current || data.length === 0) return;
|
||||
|
||||
try {
|
||||
// 创建图表 (lightweight-charts 5.0 标准 API)
|
||||
const chart = createChart(chartContainerRef.current, {
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
height: 500,
|
||||
layout: {
|
||||
background: { type: 'solid', color: themeColors.bg.card },
|
||||
textColor: themeColors.text.secondary,
|
||||
},
|
||||
grid: {
|
||||
vertLines: {
|
||||
color: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
horzLines: {
|
||||
color: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
},
|
||||
crosshair: {
|
||||
vertLine: {
|
||||
color: themeColors.primary.gold,
|
||||
width: 1,
|
||||
style: 3, // 虚线
|
||||
labelBackgroundColor: themeColors.primary.gold,
|
||||
},
|
||||
horzLine: {
|
||||
color: themeColors.primary.gold,
|
||||
width: 1,
|
||||
style: 3,
|
||||
labelBackgroundColor: themeColors.primary.gold,
|
||||
},
|
||||
},
|
||||
rightPriceScale: {
|
||||
borderColor: themeColors.border.default,
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: themeColors.border.default,
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
rightOffset: 12,
|
||||
barSpacing: 6, // 增加条形间距,减少拥挤
|
||||
fixLeftEdge: false,
|
||||
lockVisibleTimeRangeOnResize: true,
|
||||
rightBarStaysOnScroll: true,
|
||||
borderVisible: true,
|
||||
visible: true,
|
||||
// 控制时间标签的最小间距(像素)
|
||||
tickMarkMaxCharacterLength: 8,
|
||||
},
|
||||
localization: {
|
||||
locale: 'en-US',
|
||||
// 使用 ISO 日期格式,强制显示 YYYY-MM-DD
|
||||
dateFormat: 'dd MMM \'yy', // 这会被我们的自定义格式化器覆盖
|
||||
},
|
||||
handleScroll: {
|
||||
mouseWheel: true,
|
||||
pressedMouseMove: true,
|
||||
},
|
||||
handleScale: {
|
||||
axisPressedMouseMove: true,
|
||||
mouseWheel: true,
|
||||
pinch: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 设置时间轴的自定义格式化器(强制显示 YYYY-MM-DD)
|
||||
chart.applyOptions({
|
||||
localization: {
|
||||
timeFormatter: (time) => {
|
||||
// time 可能是字符串 'YYYY-MM-DD' 或时间戳
|
||||
if (typeof time === 'string') {
|
||||
return time; // 直接返回 YYYY-MM-DD 字符串
|
||||
}
|
||||
|
||||
// 如果是时间戳,转换为 YYYY-MM-DD
|
||||
const date = new Date(time * 1000);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 创建折线系列 (lightweight-charts 5.0 使用 addSeries 方法)
|
||||
// 第一个参数是 series 类本身(不是实例)
|
||||
const lineSeries = chart.addSeries(LineSeries, {
|
||||
color: themeColors.primary.gold,
|
||||
lineWidth: 2,
|
||||
crosshairMarkerVisible: true,
|
||||
crosshairMarkerRadius: 6,
|
||||
crosshairMarkerBorderColor: themeColors.primary.goldLight,
|
||||
crosshairMarkerBackgroundColor: themeColors.primary.gold,
|
||||
lastValueVisible: true,
|
||||
priceLineVisible: true,
|
||||
priceLineColor: themeColors.primary.gold,
|
||||
priceLineWidth: 1,
|
||||
priceLineStyle: 3, // 虚线
|
||||
title: metricName,
|
||||
});
|
||||
|
||||
// 转换数据格式
|
||||
// lightweight-charts 5.0 需要 YYYY-MM-DD 格式的字符串作为 time
|
||||
const chartData: LineData[] = data
|
||||
.filter((item) => item.value !== null)
|
||||
.map((item) => {
|
||||
// 确保日期格式为 YYYY-MM-DD
|
||||
const dateStr = item.date.trim();
|
||||
return {
|
||||
time: dateStr as Time,
|
||||
value: item.value as number,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// 确保时间从左到右递增
|
||||
const timeA = new Date(a.time as string).getTime();
|
||||
const timeB = new Date(b.time as string).getTime();
|
||||
return timeA - timeB;
|
||||
});
|
||||
|
||||
// 设置数据
|
||||
lineSeries.setData(chartData);
|
||||
|
||||
// 自动缩放到合适的视图
|
||||
chart.timeScale().fitContent();
|
||||
|
||||
chartRef.current = chart;
|
||||
lineSeriesRef.current = lineSeries;
|
||||
|
||||
// 响应式调整
|
||||
const handleResize = () => {
|
||||
if (chartContainerRef.current && chart) {
|
||||
chart.applyOptions({
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chart.remove();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ TradingView Chart 初始化失败:', error);
|
||||
console.error('Error details:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
createChartType: typeof createChart,
|
||||
LineSeriesType: typeof LineSeries,
|
||||
});
|
||||
// 重新抛出错误让 ErrorBoundary 捕获
|
||||
throw error;
|
||||
}
|
||||
}, [data, metricName]);
|
||||
|
||||
// 时间范围筛选
|
||||
const handleTimeRangeChange = (range: TimeRange) => {
|
||||
setSelectedRange(range);
|
||||
|
||||
if (!chartRef.current || data.length === 0) return;
|
||||
|
||||
const now = new Date();
|
||||
let startDate: Date;
|
||||
|
||||
switch (range) {
|
||||
case '1M':
|
||||
startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
|
||||
break;
|
||||
case '3M':
|
||||
startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
|
||||
break;
|
||||
case '6M':
|
||||
startDate = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate());
|
||||
break;
|
||||
case '1Y':
|
||||
startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
|
||||
break;
|
||||
case 'YTD':
|
||||
startDate = new Date(now.getFullYear(), 0, 1); // 当年1月1日
|
||||
break;
|
||||
case 'ALL':
|
||||
default:
|
||||
chartRef.current.timeScale().fitContent();
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置可见范围
|
||||
const startTimestamp = startDate.getTime() / 1000;
|
||||
const endTimestamp = now.getTime() / 1000;
|
||||
|
||||
chartRef.current.timeScale().setVisibleRange({
|
||||
from: startTimestamp as Time,
|
||||
to: endTimestamp as Time,
|
||||
});
|
||||
};
|
||||
|
||||
// 重置缩放
|
||||
const handleReset = () => {
|
||||
if (chartRef.current) {
|
||||
chartRef.current.timeScale().fitContent();
|
||||
setSelectedRange('ALL');
|
||||
}
|
||||
};
|
||||
|
||||
// 截图功能
|
||||
const handleScreenshot = () => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
const canvas = chartContainerRef.current?.querySelector('canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return;
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${metricName}_${new Date().toISOString().split('T')[0]}.png`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
};
|
||||
|
||||
// 全屏切换
|
||||
const toggleFullscreen = () => {
|
||||
if (!chartContainerRef.current) return;
|
||||
|
||||
if (!isFullscreen) {
|
||||
if (chartContainerRef.current.requestFullscreen) {
|
||||
chartContainerRef.current.requestFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
setIsFullscreen(!isFullscreen);
|
||||
};
|
||||
|
||||
// 计算统计数据
|
||||
const stats = React.useMemo(() => {
|
||||
const values = data.filter((item) => item.value !== null).map((item) => item.value as number);
|
||||
|
||||
if (values.length === 0) {
|
||||
return { min: 0, max: 0, avg: 0, latest: 0, change: 0, changePercent: 0 };
|
||||
}
|
||||
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const avg = values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
const latest = values[values.length - 1];
|
||||
const first = values[0];
|
||||
const change = latest - first;
|
||||
const changePercent = first !== 0 ? (change / first) * 100 : 0;
|
||||
|
||||
return { min, max, avg, latest, change, changePercent };
|
||||
}, [data]);
|
||||
|
||||
// 格式化数字
|
||||
const formatNumber = (num: number) => {
|
||||
if (Math.abs(num) >= 1e9) {
|
||||
return (num / 1e9).toFixed(2) + 'B';
|
||||
}
|
||||
if (Math.abs(num) >= 1e6) {
|
||||
return (num / 1e6).toFixed(2) + 'M';
|
||||
}
|
||||
if (Math.abs(num) >= 1e3) {
|
||||
return (num / 1e3).toFixed(2) + 'K';
|
||||
}
|
||||
return num.toFixed(2);
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack align="stretch" spacing={4} w="100%">
|
||||
{/* 工具栏 */}
|
||||
<Flex justify="space-between" align="center" wrap="wrap" gap={4}>
|
||||
{/* 时间范围选择 */}
|
||||
<ButtonGroup size="sm" isAttached variant="outline">
|
||||
{(['1M', '3M', '6M', '1Y', 'YTD', 'ALL'] as TimeRange[]).map((range) => (
|
||||
<Button
|
||||
key={range}
|
||||
onClick={() => handleTimeRangeChange(range)}
|
||||
bg={selectedRange === range ? themeColors.primary.gold : 'transparent'}
|
||||
color={
|
||||
selectedRange === range ? themeColors.bg.primary : themeColors.text.secondary
|
||||
}
|
||||
borderColor={themeColors.border.gold}
|
||||
_hover={{
|
||||
bg: selectedRange === range ? themeColors.primary.goldLight : themeColors.bg.card,
|
||||
}}
|
||||
>
|
||||
{range}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
{/* 图表操作 */}
|
||||
<HStack spacing={2}>
|
||||
<Tooltip label="重置视图">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color={themeColors.text.secondary}
|
||||
_hover={{ color: themeColors.primary.gold }}
|
||||
onClick={handleReset}
|
||||
>
|
||||
<Icon as={FaRedo} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label="截图">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color={themeColors.text.secondary}
|
||||
_hover={{ color: themeColors.primary.gold }}
|
||||
onClick={handleScreenshot}
|
||||
>
|
||||
<Icon as={FaCamera} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color={themeColors.text.secondary}
|
||||
_hover={{ color: themeColors.primary.gold }}
|
||||
onClick={toggleFullscreen}
|
||||
>
|
||||
<Icon as={isFullscreen ? FaCompress : FaExpand} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<Flex
|
||||
justify="space-around"
|
||||
align="center"
|
||||
bg={themeColors.bg.secondary}
|
||||
p={3}
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor={themeColors.border.default}
|
||||
wrap="wrap"
|
||||
gap={4}
|
||||
>
|
||||
<VStack spacing={0}>
|
||||
<Text color={themeColors.text.muted} fontSize="xs">
|
||||
最新值
|
||||
</Text>
|
||||
<Text color={themeColors.text.gold} fontSize="lg" fontWeight="bold">
|
||||
{formatNumber(stats.latest)} {unit}
|
||||
</Text>
|
||||
<Text
|
||||
color={stats.change >= 0 ? '#00ff88' : '#ff4444'}
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{stats.change >= 0 ? '+' : ''}
|
||||
{formatNumber(stats.change)} ({stats.changePercent.toFixed(2)}%)
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<VStack spacing={0}>
|
||||
<Text color={themeColors.text.muted} fontSize="xs">
|
||||
平均值
|
||||
</Text>
|
||||
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||
{formatNumber(stats.avg)} {unit}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<VStack spacing={0}>
|
||||
<Text color={themeColors.text.muted} fontSize="xs">
|
||||
最高值
|
||||
</Text>
|
||||
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||
{formatNumber(stats.max)} {unit}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<VStack spacing={0}>
|
||||
<Text color={themeColors.text.muted} fontSize="xs">
|
||||
最低值
|
||||
</Text>
|
||||
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||
{formatNumber(stats.min)} {unit}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<VStack spacing={0}>
|
||||
<Text color={themeColors.text.muted} fontSize="xs">
|
||||
数据点数
|
||||
</Text>
|
||||
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||
{data.filter((item) => item.value !== null).length}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<VStack spacing={0}>
|
||||
<Text color={themeColors.text.muted} fontSize="xs">
|
||||
频率
|
||||
</Text>
|
||||
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||
{frequency}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
|
||||
{/* 图表容器 */}
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
w="100%"
|
||||
h="500px"
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor={themeColors.border.gold}
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
bg={themeColors.bg.card}
|
||||
/>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<Flex justify="space-between" align="center" fontSize="xs" color={themeColors.text.muted}>
|
||||
<HStack spacing={4}>
|
||||
<Text>💡 提示:滚动鼠标滚轮缩放,拖拽移动视图</Text>
|
||||
</HStack>
|
||||
<Text>数据来源: {metricName}</Text>
|
||||
</Flex>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradingViewChart;
|
||||
@@ -696,18 +696,20 @@ const DataBrowser: React.FC = () => {
|
||||
p={3}
|
||||
cursor="pointer"
|
||||
bg="transparent"
|
||||
_hover={{ bg: themeColors.bg.cardHover }}
|
||||
_hover={{
|
||||
bg: themeColors.bg.cardHover,
|
||||
borderLeftColor: themeColors.primary.gold,
|
||||
}}
|
||||
borderRadius="md"
|
||||
borderLeftWidth="3px"
|
||||
borderLeftColor="transparent"
|
||||
_hover={{ borderLeftColor: themeColors.primary.gold }}
|
||||
transition="all 0.2s"
|
||||
onClick={() => {
|
||||
// 转换搜索结果为 TreeMetric 格式
|
||||
const metric: TreeMetric = {
|
||||
metric_id: result.metric_id,
|
||||
metric_name: result.metric_name,
|
||||
source: result.source,
|
||||
source: result.source as 'SMM' | 'Mysteel',
|
||||
frequency: result.frequency,
|
||||
unit: result.unit,
|
||||
description: result.description,
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
// src/views/Home/HomePage.js - 专业投资分析平台
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Heading,
|
||||
Text,
|
||||
Card,
|
||||
CardBody,
|
||||
Badge,
|
||||
Button,
|
||||
Flex,
|
||||
VStack,
|
||||
HStack,
|
||||
SimpleGrid,
|
||||
useBreakpointValue
|
||||
} from '@chakra-ui/react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import heroBg from '../../assets/img/BackgroundCard1.png';
|
||||
import '../../styles/home-animations.css';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { usePostHogTrack } from '../../hooks/usePostHogRedux';
|
||||
import { ACQUISITION_EVENTS } from '../../lib/constants';
|
||||
|
||||
export default function HomePage() {
|
||||
const { user, isAuthenticated } = useAuth(); // ⚡ 移除 isLoading,不再依赖它
|
||||
const navigate = useNavigate();
|
||||
const { track } = usePostHogTrack(); // PostHog 追踪
|
||||
const [imageLoaded, setImageLoaded] = React.useState(false);
|
||||
|
||||
// 响应式配置
|
||||
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 });
|
||||
|
||||
// 保留原有的调试信息
|
||||
useEffect(() => {
|
||||
logger.debug('HomePage', 'AuthContext状态', {
|
||||
userId: user?.id,
|
||||
username: user?.username,
|
||||
nickname: user?.nickname,
|
||||
isAuthenticated,
|
||||
hasUser: !!user
|
||||
});
|
||||
}, [user?.id, isAuthenticated]); // 只依赖 user.id,避免无限循环
|
||||
|
||||
// 🎯 PostHog 追踪:页面浏览
|
||||
useEffect(() => {
|
||||
track(ACQUISITION_EVENTS.LANDING_PAGE_VIEWED, {
|
||||
timestamp: new Date().toISOString(),
|
||||
is_authenticated: isAuthenticated,
|
||||
user_id: user?.id || null,
|
||||
});
|
||||
}, [track, isAuthenticated, user?.id]);
|
||||
|
||||
// 核心功能配置 - 5个主要功能
|
||||
const coreFeatures = [
|
||||
{
|
||||
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: '实战'
|
||||
}
|
||||
];
|
||||
|
||||
// @TODO 如何区分内部链接和外部链接?
|
||||
const handleProductClick = useCallback((feature) => {
|
||||
// 🎯 PostHog 追踪:功能卡片点击
|
||||
track(ACQUISITION_EVENTS.FEATURE_CARD_CLICKED, {
|
||||
feature_id: feature.id,
|
||||
feature_title: feature.title,
|
||||
feature_url: feature.url,
|
||||
is_featured: feature.featured || false,
|
||||
link_type: feature.url.startsWith('http') ? 'external' : 'internal',
|
||||
});
|
||||
|
||||
// 原有导航逻辑
|
||||
if (feature.url.startsWith('http')) {
|
||||
// 外部链接,直接打开
|
||||
window.open(feature.url, '_blank');
|
||||
} else {
|
||||
// 内部路由
|
||||
navigate(feature.url);
|
||||
}
|
||||
}, [track, navigate]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* 开发调试信息 */}
|
||||
{/* {process.env.NODE_ENV === 'development' && (
|
||||
<Box bg="rgba(0, 0, 0, 0.8)" color="yellow.200" p={2} fontSize="xs" zIndex={1000} position="relative">
|
||||
认证: {isAuthenticated ? '✅' : '❌'} | 用户: {user?.nickname || '无'}
|
||||
</Box>
|
||||
)} */}
|
||||
|
||||
{/* Hero Section - 深色科技风格 */}
|
||||
<Box
|
||||
position="relative"
|
||||
minH={heroHeight}
|
||||
bg="linear-gradient(135deg, #0E0C15 0%, #15131D 50%, #252134 100%)"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* 背景图片和装饰 - 优化:延迟加载 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
right="0"
|
||||
w="50%"
|
||||
h="100%"
|
||||
bgImage={imageLoaded ? `url(${heroBg})` : 'none'}
|
||||
bgSize="cover"
|
||||
bgPosition="center"
|
||||
opacity={imageLoaded ? 0.3 : 0}
|
||||
transition="opacity 0.5s ease-in"
|
||||
_after={{
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(90deg, rgba(14, 12, 21, 0.9) 0%, rgba(14, 12, 21, 0.3) 100%)'
|
||||
}}
|
||||
/>
|
||||
{/* 预加载背景图片 */}
|
||||
<Box display="none">
|
||||
<img
|
||||
src={heroBg}
|
||||
alt=""
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
onError={() => setImageLoaded(true)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 装饰性几何图形 - 移动端隐藏 */}
|
||||
{showDecorations && (
|
||||
<>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="10%"
|
||||
w={{ base: '100px', md: '150px', lg: '200px' }}
|
||||
h={{ base: '100px', md: '150px', lg: '200px' }}
|
||||
borderRadius="50%"
|
||||
bg="rgba(255, 215, 0, 0.1)"
|
||||
filter="blur(80px)"
|
||||
className="float-animation"
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="30%"
|
||||
right="20%"
|
||||
w={{ base: '80px', md: '120px', lg: '150px' }}
|
||||
h={{ base: '80px', md: '120px', lg: '150px' }}
|
||||
borderRadius="50%"
|
||||
bg="rgba(138, 43, 226, 0.1)"
|
||||
filter="blur(60px)"
|
||||
className="float-animation-reverse"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Container maxW="7xl" position="relative" zIndex={30} px={containerPx}>
|
||||
<VStack spacing={{ base: 8, md: 12, lg: 16 }} align="stretch" minH={heroHeight} justify="center">
|
||||
{/* 主标题区域 */}
|
||||
<VStack spacing={{ base: 4, md: 5, lg: 6 }} textAlign="center" pt={{ base: 4, md: 6, lg: 8 }}>
|
||||
<Heading
|
||||
size={headingSize}
|
||||
color="white"
|
||||
fontWeight="900"
|
||||
letterSpacing={headingLetterSpacing}
|
||||
lineHeight="shorter"
|
||||
>
|
||||
智能投资分析平台
|
||||
</Heading>
|
||||
<Text fontSize={heroTextSize} color="whiteAlpha.800" maxW={{ base: '100%', md: '2xl', lg: '3xl' }} lineHeight="tall" px={{ base: 4, md: 0 }}>
|
||||
专业投资研究工具,助您把握市场机遇
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
|
||||
{/* 核心功能面板 */}
|
||||
<Box pb={{ base: 8, md: 12 }}>
|
||||
<VStack spacing={{ base: 6, md: 8 }}>
|
||||
|
||||
{/* 新闻中心 - 突出显示 */}
|
||||
<Card
|
||||
bg="transparent"
|
||||
border="2px solid"
|
||||
borderColor="yellow.400"
|
||||
borderRadius={{ base: '2xl', md: '3xl' }}
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
shadow="2xl"
|
||||
w="100%"
|
||||
_before={{
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 165, 0, 0.05) 100%)',
|
||||
zIndex: 0
|
||||
}}
|
||||
>
|
||||
<CardBody p={{ base: 6, md: 8 }} position="relative" zIndex={1}>
|
||||
{/* 响应式布局:移动端纵向,桌面端横向 */}
|
||||
<Flex
|
||||
direction={{ base: 'column', md: 'row' }}
|
||||
align={{ base: 'stretch', md: 'center' }}
|
||||
justify={{ base: 'flex-start', md: 'space-between' }}
|
||||
gap={{ base: 4, md: 6 }}
|
||||
>
|
||||
<Flex align="center" gap={{ base: 4, md: 6 }} flex={1}>
|
||||
<Box
|
||||
p={{ base: 3, md: 4 }}
|
||||
borderRadius={{ base: 'lg', md: 'xl' }}
|
||||
bg="yellow.400"
|
||||
color="black"
|
||||
>
|
||||
<Text fontSize={{ base: '2xl', md: '3xl' }}>{coreFeatures[0].icon}</Text>
|
||||
</Box>
|
||||
<VStack align="start" spacing={{ base: 1, md: 2 }} flex={1}>
|
||||
<HStack>
|
||||
<Heading size={{ base: 'lg', md: 'xl' }} color="white">
|
||||
{coreFeatures[0].title}
|
||||
</Heading>
|
||||
<Badge colorScheme="yellow" variant="solid" fontSize={{ base: 'xs', md: 'sm' }}>
|
||||
{coreFeatures[0].badge}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text color="whiteAlpha.800" fontSize={{ base: 'md', md: 'lg' }} maxW={{ md: 'md' }} lineHeight="tall">
|
||||
{coreFeatures[0].description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size={{ base: 'md', md: 'lg' }}
|
||||
borderRadius="full"
|
||||
fontWeight="bold"
|
||||
w={{ base: '100%', md: 'auto' }}
|
||||
onClick={() => handleProductClick(coreFeatures[0])}
|
||||
minH="44px"
|
||||
flexShrink={0}
|
||||
>
|
||||
进入功能 →
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 其他5个功能 */}
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={{ base: 4, md: 5, lg: 6 }} w="100%">
|
||||
{coreFeatures.slice(1).map((feature) => (
|
||||
<Card
|
||||
key={feature.id}
|
||||
bg="whiteAlpha.100"
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.200"
|
||||
borderRadius={{ base: 'xl', md: '2xl' }}
|
||||
transition="all 0.3s ease"
|
||||
_hover={{
|
||||
bg: 'whiteAlpha.200',
|
||||
borderColor: `${feature.color}.400`,
|
||||
transform: 'translateY(-4px)',
|
||||
shadow: '2xl'
|
||||
}}
|
||||
_active={{
|
||||
bg: 'whiteAlpha.200',
|
||||
borderColor: `${feature.color}.400`,
|
||||
transform: 'translateY(-2px)'
|
||||
}}
|
||||
onClick={() => handleProductClick(feature)}
|
||||
minH={{ base: 'auto', md: '180px' }}
|
||||
>
|
||||
<CardBody p={{ base: 5, md: 6 }}>
|
||||
<VStack spacing={{ base: 3, md: 4 }} align="start" h="100%">
|
||||
<HStack>
|
||||
<Box
|
||||
p={{ base: 2, md: 3 }}
|
||||
borderRadius="lg"
|
||||
bg={`${feature.color}.50`}
|
||||
border="1px solid"
|
||||
borderColor={`${feature.color}.200`}
|
||||
>
|
||||
<Text fontSize={{ base: 'xl', md: '2xl' }}>{feature.icon}</Text>
|
||||
</Box>
|
||||
<Badge colorScheme={feature.color} variant="solid" fontSize={{ base: 'xs', md: 'sm' }}>
|
||||
{feature.badge}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<VStack align="start" spacing={{ base: 1, md: 2 }} flex={1}>
|
||||
<Heading size={{ base: 'md', md: 'lg' }} color="white">
|
||||
{feature.title}
|
||||
</Heading>
|
||||
<Text color="whiteAlpha.800" fontSize={{ base: 'xs', md: 'sm' }} lineHeight="tall">
|
||||
{feature.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<Button
|
||||
colorScheme={feature.color}
|
||||
size={{ base: 'md', md: 'sm' }}
|
||||
variant="outline"
|
||||
alignSelf="flex-end"
|
||||
w={{ base: '100%', md: 'auto' }}
|
||||
minH="44px"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleProductClick(feature);
|
||||
}}
|
||||
>
|
||||
使用
|
||||
</Button>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
147
src/views/Home/HomePage.tsx
Normal file
147
src/views/Home/HomePage.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
// src/views/Home/HomePage.tsx
|
||||
// 首页 - 专业投资分析平台
|
||||
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { Box, Container, VStack, SimpleGrid } from '@chakra-ui/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { usePostHogTrack } from '@/hooks/usePostHogRedux';
|
||||
import { useHomeResponsive } from '@/hooks/useHomeResponsive';
|
||||
import { ACQUISITION_EVENTS } from '@/lib/constants';
|
||||
import { CORE_FEATURES } from '@/constants/homeFeatures';
|
||||
import { performanceMonitor } from '@/utils/performanceMonitor';
|
||||
import type { Feature } from '@/types/home';
|
||||
import { HeroBackground } from './components/HeroBackground';
|
||||
import { HeroHeader } from './components/HeroHeader';
|
||||
import { FeaturedFeatureCard } from './components/FeaturedFeatureCard';
|
||||
import { FeatureCard } from './components/FeatureCard';
|
||||
import '@/styles/home-animations.css';
|
||||
|
||||
/**
|
||||
* 首页组件
|
||||
* 展示平台核心功能,引导用户探索各个功能模块
|
||||
*/
|
||||
const HomePage: React.FC = () => {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { track } = usePostHogTrack();
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
// 响应式配置
|
||||
const {
|
||||
heroHeight,
|
||||
headingSize,
|
||||
headingLetterSpacing,
|
||||
heroTextSize,
|
||||
containerPx,
|
||||
showDecorations
|
||||
} = useHomeResponsive();
|
||||
|
||||
// ⚡ 性能标记:首页组件挂载 = 渲染开始
|
||||
useEffect(() => {
|
||||
performanceMonitor.mark('homepage-render-start');
|
||||
}, []);
|
||||
|
||||
// PostHog 追踪:页面浏览
|
||||
useEffect(() => {
|
||||
track(ACQUISITION_EVENTS.LANDING_PAGE_VIEWED, {
|
||||
timestamp: new Date().toISOString(),
|
||||
is_authenticated: isAuthenticated,
|
||||
user_id: user?.id || null,
|
||||
});
|
||||
}, [track, isAuthenticated, user?.id]);
|
||||
|
||||
// 功能卡片点击处理
|
||||
const handleFeatureClick = useCallback((feature: Feature) => {
|
||||
// PostHog 追踪:功能卡片点击
|
||||
track(ACQUISITION_EVENTS.FEATURE_CARD_VIEWED, {
|
||||
feature_id: feature.id,
|
||||
feature_title: feature.title,
|
||||
feature_url: feature.url,
|
||||
is_featured: feature.featured || false,
|
||||
link_type: feature.url.startsWith('http') ? 'external' : 'internal',
|
||||
});
|
||||
|
||||
// 导航处理
|
||||
if (feature.url.startsWith('http')) {
|
||||
window.open(feature.url, '_blank');
|
||||
} else {
|
||||
navigate(feature.url);
|
||||
}
|
||||
}, [track, navigate]);
|
||||
|
||||
// 背景图片加载完成回调
|
||||
const handleImageLoad = useCallback(() => {
|
||||
setImageLoaded(true);
|
||||
// ⚡ 性能标记:首页渲染完成(背景图片加载完成 = 首屏视觉完整)
|
||||
performanceMonitor.mark('homepage-render-end');
|
||||
}, []);
|
||||
|
||||
// 特色功能(第一个)
|
||||
const featuredFeature = CORE_FEATURES[0];
|
||||
// 其他功能
|
||||
const regularFeatures = CORE_FEATURES.slice(1);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Hero Section - 深色科技风格 */}
|
||||
<Box
|
||||
position="relative"
|
||||
minH={heroHeight}
|
||||
bg="linear-gradient(135deg, #0E0C15 0%, #15131D 50%, #252134 100%)"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* 背景装饰 */}
|
||||
<HeroBackground
|
||||
imageLoaded={imageLoaded}
|
||||
onImageLoad={handleImageLoad}
|
||||
showDecorations={showDecorations}
|
||||
/>
|
||||
|
||||
<Container maxW="7xl" position="relative" zIndex={30} px={containerPx}>
|
||||
<VStack
|
||||
spacing={{ base: 8, md: 12, lg: 16 }}
|
||||
align="stretch"
|
||||
minH={heroHeight}
|
||||
justify="center"
|
||||
>
|
||||
{/* 主标题区域 */}
|
||||
<HeroHeader
|
||||
headingSize={headingSize}
|
||||
headingLetterSpacing={headingLetterSpacing}
|
||||
heroTextSize={heroTextSize}
|
||||
/>
|
||||
|
||||
{/* 核心功能面板 */}
|
||||
<Box pb={{ base: 8, md: 12 }}>
|
||||
<VStack spacing={{ base: 6, md: 8 }}>
|
||||
{/* 特色功能卡片 - 新闻中心 */}
|
||||
<FeaturedFeatureCard
|
||||
feature={featuredFeature}
|
||||
onClick={handleFeatureClick}
|
||||
/>
|
||||
|
||||
{/* 其他功能卡片 */}
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, md: 2, lg: 3 }}
|
||||
spacing={{ base: 4, md: 5, lg: 6 }}
|
||||
w="100%"
|
||||
>
|
||||
{regularFeatures.map((feature) => (
|
||||
<FeatureCard
|
||||
key={feature.id}
|
||||
feature={feature}
|
||||
onClick={handleFeatureClick}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
106
src/views/Home/components/FeatureCard.tsx
Normal file
106
src/views/Home/components/FeatureCard.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
// src/views/Home/components/FeatureCard.tsx
|
||||
// 普通功能卡片组件
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
VStack,
|
||||
HStack,
|
||||
Box,
|
||||
Heading,
|
||||
Text,
|
||||
Badge,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
import type { Feature, FeatureClickHandler } from '@/types/home';
|
||||
|
||||
interface FeatureCardProps {
|
||||
feature: Feature;
|
||||
onClick: FeatureClickHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通功能卡片组件
|
||||
* 用于展示除特色功能外的其他功能
|
||||
*/
|
||||
export const FeatureCard: React.FC<FeatureCardProps> = ({
|
||||
feature,
|
||||
onClick
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
bg="whiteAlpha.100"
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.200"
|
||||
borderRadius={{ base: 'xl', md: '2xl' }}
|
||||
transition="all 0.3s ease"
|
||||
_hover={{
|
||||
bg: 'whiteAlpha.200',
|
||||
borderColor: `${feature.color}.400`,
|
||||
transform: 'translateY(-4px)',
|
||||
shadow: '2xl'
|
||||
}}
|
||||
_active={{
|
||||
bg: 'whiteAlpha.200',
|
||||
borderColor: `${feature.color}.400`,
|
||||
transform: 'translateY(-2px)'
|
||||
}}
|
||||
onClick={() => onClick(feature)}
|
||||
minH={{ base: 'auto', md: '180px' }}
|
||||
cursor="pointer"
|
||||
>
|
||||
<CardBody p={{ base: 5, md: 6 }}>
|
||||
<VStack spacing={{ base: 3, md: 4 }} align="start" h="100%">
|
||||
<HStack>
|
||||
<Box
|
||||
p={{ base: 2, md: 3 }}
|
||||
borderRadius="lg"
|
||||
bg={`${feature.color}.50`}
|
||||
border="1px solid"
|
||||
borderColor={`${feature.color}.200`}
|
||||
>
|
||||
<Text fontSize={{ base: 'xl', md: '2xl' }}>{feature.icon}</Text>
|
||||
</Box>
|
||||
<Badge
|
||||
colorScheme={feature.color}
|
||||
variant="solid"
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
>
|
||||
{feature.badge}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<VStack align="start" spacing={{ base: 1, md: 2 }} flex={1}>
|
||||
<Heading size={{ base: 'md', md: 'lg' }} color="white">
|
||||
{feature.title}
|
||||
</Heading>
|
||||
<Text
|
||||
color="whiteAlpha.800"
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
lineHeight="tall"
|
||||
>
|
||||
{feature.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<Button
|
||||
colorScheme={feature.color}
|
||||
size={{ base: 'md', md: 'sm' }}
|
||||
variant="outline"
|
||||
alignSelf="flex-end"
|
||||
w={{ base: '100%', md: 'auto' }}
|
||||
minH="44px"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick(feature);
|
||||
}}
|
||||
>
|
||||
使用
|
||||
</Button>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
104
src/views/Home/components/FeaturedFeatureCard.tsx
Normal file
104
src/views/Home/components/FeaturedFeatureCard.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
// src/views/Home/components/FeaturedFeatureCard.tsx
|
||||
// 特色功能卡片组件(新闻中心)
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
Flex,
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Heading,
|
||||
Text,
|
||||
Badge,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
import type { Feature, FeatureClickHandler } from '@/types/home';
|
||||
|
||||
interface FeaturedFeatureCardProps {
|
||||
feature: Feature;
|
||||
onClick: FeatureClickHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 特色功能卡片组件
|
||||
* 用于突出显示最重要的功能(如新闻中心)
|
||||
*/
|
||||
export const FeaturedFeatureCard: React.FC<FeaturedFeatureCardProps> = ({
|
||||
feature,
|
||||
onClick
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
bg="transparent"
|
||||
border="2px solid"
|
||||
borderColor="yellow.400"
|
||||
borderRadius={{ base: '2xl', md: '3xl' }}
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
shadow="2xl"
|
||||
w="100%"
|
||||
_before={{
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 165, 0, 0.05) 100%)',
|
||||
zIndex: 0
|
||||
}}
|
||||
>
|
||||
<CardBody p={{ base: 6, md: 8 }} position="relative" zIndex={1}>
|
||||
<Flex
|
||||
direction={{ base: 'column', md: 'row' }}
|
||||
align={{ base: 'stretch', md: 'center' }}
|
||||
justify={{ base: 'flex-start', md: 'space-between' }}
|
||||
gap={{ base: 4, md: 6 }}
|
||||
>
|
||||
<Flex align="center" gap={{ base: 4, md: 6 }} flex={1}>
|
||||
<Box
|
||||
p={{ base: 3, md: 4 }}
|
||||
borderRadius={{ base: 'lg', md: 'xl' }}
|
||||
bg="yellow.400"
|
||||
color="black"
|
||||
>
|
||||
<Text fontSize={{ base: '2xl', md: '3xl' }}>{feature.icon}</Text>
|
||||
</Box>
|
||||
<VStack align="start" spacing={{ base: 1, md: 2 }} flex={1}>
|
||||
<HStack>
|
||||
<Heading size={{ base: 'lg', md: 'xl' }} color="white">
|
||||
{feature.title}
|
||||
</Heading>
|
||||
<Badge colorScheme="yellow" variant="solid" fontSize={{ base: 'xs', md: 'sm' }}>
|
||||
{feature.badge}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text
|
||||
color="whiteAlpha.800"
|
||||
fontSize={{ base: 'md', md: 'lg' }}
|
||||
maxW={{ md: 'md' }}
|
||||
lineHeight="tall"
|
||||
>
|
||||
{feature.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size={{ base: 'md', md: 'lg' }}
|
||||
borderRadius="full"
|
||||
fontWeight="bold"
|
||||
w={{ base: '100%', md: 'auto' }}
|
||||
onClick={() => onClick(feature)}
|
||||
minH="44px"
|
||||
flexShrink={0}
|
||||
>
|
||||
进入功能 →
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
87
src/views/Home/components/HeroBackground.tsx
Normal file
87
src/views/Home/components/HeroBackground.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
// src/views/Home/components/HeroBackground.tsx
|
||||
// 首页英雄区背景装饰组件
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import heroBg from '@assets/img/BackgroundCard1.png';
|
||||
|
||||
interface HeroBackgroundProps {
|
||||
imageLoaded: boolean;
|
||||
onImageLoad: () => void;
|
||||
showDecorations: boolean | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页英雄区背景组件
|
||||
* 包含背景图片和装饰性几何图形
|
||||
*/
|
||||
export const HeroBackground: React.FC<HeroBackgroundProps> = ({
|
||||
imageLoaded,
|
||||
onImageLoad,
|
||||
showDecorations
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* 背景图片 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
right="0"
|
||||
w="50%"
|
||||
h="100%"
|
||||
bgImage={imageLoaded ? `url(${heroBg})` : 'none'}
|
||||
bgSize="cover"
|
||||
bgPosition="center"
|
||||
opacity={imageLoaded ? 0.3 : 0}
|
||||
transition="opacity 0.5s ease-in"
|
||||
_after={{
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(90deg, rgba(14, 12, 21, 0.9) 0%, rgba(14, 12, 21, 0.3) 100%)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 预加载背景图片 */}
|
||||
<Box display="none">
|
||||
<img
|
||||
src={heroBg}
|
||||
alt=""
|
||||
onLoad={onImageLoad}
|
||||
onError={onImageLoad}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 装饰性几何图形 - 移动端隐藏 */}
|
||||
{showDecorations && (
|
||||
<>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="10%"
|
||||
w={{ base: '100px', md: '150px', lg: '200px' }}
|
||||
h={{ base: '100px', md: '150px', lg: '200px' }}
|
||||
borderRadius="50%"
|
||||
bg="rgba(255, 215, 0, 0.1)"
|
||||
filter="blur(80px)"
|
||||
className="float-animation"
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="30%"
|
||||
right="20%"
|
||||
w={{ base: '80px', md: '120px', lg: '150px' }}
|
||||
h={{ base: '80px', md: '120px', lg: '150px' }}
|
||||
borderRadius="50%"
|
||||
bg="rgba(138, 43, 226, 0.1)"
|
||||
filter="blur(60px)"
|
||||
className="float-animation-reverse"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
48
src/views/Home/components/HeroHeader.tsx
Normal file
48
src/views/Home/components/HeroHeader.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
// src/views/Home/components/HeroHeader.tsx
|
||||
// 首页主标题区域组件
|
||||
|
||||
import React from 'react';
|
||||
import { Heading, Text, VStack } from '@chakra-ui/react';
|
||||
|
||||
interface HeroHeaderProps {
|
||||
headingSize: string | undefined;
|
||||
headingLetterSpacing: string | undefined;
|
||||
heroTextSize: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页主标题区域组件
|
||||
* 包含主标题和副标题
|
||||
*/
|
||||
export const HeroHeader: React.FC<HeroHeaderProps> = ({
|
||||
headingSize,
|
||||
headingLetterSpacing,
|
||||
heroTextSize
|
||||
}) => {
|
||||
return (
|
||||
<VStack
|
||||
spacing={{ base: 4, md: 5, lg: 6 }}
|
||||
textAlign="center"
|
||||
pt={{ base: 4, md: 6, lg: 8 }}
|
||||
>
|
||||
<Heading
|
||||
size={headingSize}
|
||||
color="white"
|
||||
fontWeight="900"
|
||||
letterSpacing={headingLetterSpacing}
|
||||
lineHeight="shorter"
|
||||
>
|
||||
智能投资分析平台
|
||||
</Heading>
|
||||
<Text
|
||||
fontSize={heroTextSize}
|
||||
color="whiteAlpha.800"
|
||||
maxW={{ base: '100%', md: '2xl', lg: '3xl' }}
|
||||
lineHeight="tall"
|
||||
px={{ base: 4, md: 0 }}
|
||||
>
|
||||
专业投资研究工具,助您把握市场机遇
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
@@ -1,194 +0,0 @@
|
||||
/**
|
||||
* 首页骨架屏组件
|
||||
* 模拟首页的 6 个功能卡片布局,减少白屏感知时间
|
||||
*
|
||||
* 使用 Chakra UI 的 Skeleton 组件
|
||||
*
|
||||
* @module views/Home/components/HomePageSkeleton
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
VStack,
|
||||
HStack,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
|
||||
interface HomePageSkeletonProps {
|
||||
/** 是否显示动画效果 */
|
||||
isAnimated?: boolean;
|
||||
/** 骨架屏速度(秒) */
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 单个卡片骨架
|
||||
// ============================================================
|
||||
|
||||
const FeatureCardSkeleton: React.FC<{ isFeatured?: boolean }> = ({ isFeatured = false }) => {
|
||||
const bg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={bg}
|
||||
borderRadius="xl"
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
p={isFeatured ? 8 : 6}
|
||||
h={isFeatured ? '350px' : '280px'}
|
||||
boxShadow={isFeatured ? 'xl' : 'md'}
|
||||
position="relative"
|
||||
>
|
||||
<VStack align="start" spacing={4} h="full">
|
||||
{/* 图标骨架 */}
|
||||
<Skeleton
|
||||
height={isFeatured ? '60px' : '48px'}
|
||||
width={isFeatured ? '60px' : '48px'}
|
||||
borderRadius="lg"
|
||||
startColor={isFeatured ? 'blue.100' : 'gray.100'}
|
||||
endColor={isFeatured ? 'blue.200' : 'gray.200'}
|
||||
/>
|
||||
|
||||
{/* 标题骨架 */}
|
||||
<Skeleton height="28px" width="70%" borderRadius="md" />
|
||||
|
||||
{/* 描述骨架 */}
|
||||
<SkeletonText
|
||||
mt="2"
|
||||
noOfLines={isFeatured ? 4 : 3}
|
||||
spacing="3"
|
||||
skeletonHeight="2"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
{/* 按钮骨架 */}
|
||||
<Skeleton
|
||||
height="40px"
|
||||
width={isFeatured ? '140px' : '100px'}
|
||||
borderRadius="md"
|
||||
mt="auto"
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
{/* Featured 徽章骨架 */}
|
||||
{isFeatured && (
|
||||
<Skeleton
|
||||
position="absolute"
|
||||
top="4"
|
||||
right="4"
|
||||
height="24px"
|
||||
width="80px"
|
||||
borderRadius="full"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 主骨架组件
|
||||
// ============================================================
|
||||
|
||||
export const HomePageSkeleton: React.FC<HomePageSkeletonProps> = ({
|
||||
isAnimated = true,
|
||||
speed = 0.8,
|
||||
}) => {
|
||||
const containerBg = useColorModeValue('gray.50', 'gray.900');
|
||||
|
||||
return (
|
||||
<Box
|
||||
w="full"
|
||||
minH="100vh"
|
||||
bg={containerBg}
|
||||
pt={{ base: '120px', md: '140px' }}
|
||||
pb={{ base: '60px', md: '80px' }}
|
||||
>
|
||||
<Container maxW="container.xl">
|
||||
<VStack spacing={{ base: 8, md: 12 }} align="stretch">
|
||||
{/* 顶部标题区域骨架 */}
|
||||
<VStack spacing={4} textAlign="center">
|
||||
{/* 主标题 */}
|
||||
<Skeleton
|
||||
height={{ base: '40px', md: '56px' }}
|
||||
width={{ base: '80%', md: '500px' }}
|
||||
borderRadius="md"
|
||||
speed={speed}
|
||||
/>
|
||||
|
||||
{/* 副标题 */}
|
||||
<Skeleton
|
||||
height={{ base: '20px', md: '24px' }}
|
||||
width={{ base: '90%', md: '600px' }}
|
||||
borderRadius="md"
|
||||
speed={speed}
|
||||
/>
|
||||
|
||||
{/* CTA 按钮 */}
|
||||
<HStack spacing={4} mt={4}>
|
||||
<Skeleton
|
||||
height="48px"
|
||||
width="140px"
|
||||
borderRadius="lg"
|
||||
speed={speed}
|
||||
/>
|
||||
<Skeleton
|
||||
height="48px"
|
||||
width="140px"
|
||||
borderRadius="lg"
|
||||
speed={speed}
|
||||
/>
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
{/* 功能卡片网格骨架 */}
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, md: 2, lg: 3 }}
|
||||
spacing={{ base: 6, md: 8 }}
|
||||
mt={8}
|
||||
>
|
||||
{/* 第一张卡片 - Featured (新闻中心) */}
|
||||
<Box gridColumn={{ base: 'span 1', lg: 'span 2' }}>
|
||||
<FeatureCardSkeleton isFeatured />
|
||||
</Box>
|
||||
|
||||
{/* 其余 5 张卡片 */}
|
||||
{[1, 2, 3, 4, 5].map((index) => (
|
||||
<FeatureCardSkeleton key={index} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
{/* 底部装饰元素骨架 */}
|
||||
<HStack justify="center" spacing={8} mt={12}>
|
||||
{[1, 2, 3].map((index) => (
|
||||
<VStack key={index} spacing={2} align="center">
|
||||
<Skeleton
|
||||
height="40px"
|
||||
width="40px"
|
||||
borderRadius="full"
|
||||
speed={speed}
|
||||
/>
|
||||
<Skeleton height="16px" width="60px" borderRadius="md" speed={speed} />
|
||||
</VStack>
|
||||
))}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 默认导出
|
||||
// ============================================================
|
||||
|
||||
export default HomePageSkeleton;
|
||||
@@ -37,20 +37,40 @@ export default function WechatCallback() {
|
||||
// 1. 获取URL参数
|
||||
const code = searchParams.get("code");
|
||||
const state = searchParams.get("state");
|
||||
const wechatLogin = searchParams.get("wechat_login");
|
||||
|
||||
// 2. 参数验证
|
||||
// 2. 检查是否是 H5 模式登录成功回调
|
||||
// 后端已经完成登录,只需要刷新前端 session 状态
|
||||
if (wechatLogin === "success") {
|
||||
logger.info('WechatCallback', 'H5 模式登录成功', { state });
|
||||
|
||||
// 刷新 session 状态
|
||||
await checkSession();
|
||||
|
||||
// 显示成功状态
|
||||
setStatus("success");
|
||||
setMessage("登录成功!正在跳转...");
|
||||
|
||||
// 延迟跳转到首页
|
||||
setTimeout(() => {
|
||||
navigate("/home", { replace: true });
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 原有的 code 模式处理(微信直接回调前端的情况)
|
||||
if (!code) {
|
||||
throw new Error("授权失败:缺少授权码");
|
||||
}
|
||||
|
||||
// 3. 调用后端处理回调
|
||||
// 4. 调用后端处理回调
|
||||
const response = await authService.handleWechatH5Callback(code, state);
|
||||
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.error || "授权失败,请重试");
|
||||
}
|
||||
|
||||
// 4. 存储用户信息(如果有返回token)
|
||||
// 5. 存储用户信息(如果有返回token)
|
||||
if (response.token) {
|
||||
localStorage.setItem("token", response.token);
|
||||
}
|
||||
@@ -58,14 +78,14 @@ export default function WechatCallback() {
|
||||
localStorage.setItem("user", JSON.stringify(response.user));
|
||||
}
|
||||
|
||||
// 5. 更新session
|
||||
// 6. 更新session
|
||||
await checkSession();
|
||||
|
||||
// 6. 显示成功状态
|
||||
// 7. 显示成功状态
|
||||
setStatus("success");
|
||||
setMessage("登录成功!正在跳转...");
|
||||
|
||||
// 7. 延迟跳转到首页
|
||||
// 8. 延迟跳转到首页
|
||||
setTimeout(() => {
|
||||
navigate("/home", { replace: true });
|
||||
}, 1500);
|
||||
@@ -73,6 +93,7 @@ export default function WechatCallback() {
|
||||
logger.error('WechatCallback', 'handleCallback', error, {
|
||||
code: searchParams.get("code"),
|
||||
state: searchParams.get("state"),
|
||||
wechat_login: searchParams.get("wechat_login"),
|
||||
errorMessage: error.message
|
||||
});
|
||||
setStatus("error");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { motion } from "framer-motion";
|
||||
import Button from "@/components/Button2";
|
||||
import Button from "@/components/Button";
|
||||
|
||||
import { pricing } from "./content";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user