Compare commits

...

23 Commits

Author SHA1 Message Date
zdl
376b5f66cd Merge branch 'feature' into develop
* feature:
  手机号格式适配-前端修改
  添加微信扫码的几种其他状态
  pref: 去除坏味道
  feat: 微信登陆逻辑调整
  feat: 微信mock数据调整
  整合register端口进入login端口
  整合register端口进入login端口
  整合register端口进入login端口
  feat: 文案调整
  整合register端口进入login端口
  feat: 修复首页新闻中心卡片布局跳变问题
  整合register端口进入login端口
  fix: 修复首页路由跳转失败的问题
  修改个股详情中桑基图提示Stack: Error: Sankey is a DAG
  bugfix:调整
  feat: 路由链接调整
  bugfix:修复警告错误
  feat: 错误logger 不在被error页面捕获
2025-10-29 16:27:53 +08:00
zdl
f3c7e016ac Merge branch '1028_bugfix' into feature
* 1028_bugfix:
  手机号格式适配-前端修改
  添加微信扫码的几种其他状态
  整合register端口进入login端口
2025-10-29 16:27:31 +08:00
8417ab17be 手机号格式适配-前端修改 2025-10-29 11:20:41 +08:00
dd59cb6385 添加微信扫码的几种其他状态 2025-10-29 07:33:44 +08:00
zdl
542b20368e Merge branch 'feature_2025/1028_bugfix' into feature 2025-10-28 19:41:20 +08:00
zdl
d456c3cd5f pref: 去除坏味道 2025-10-28 19:06:50 +08:00
zdl
b221c2669c feat: 微信登陆逻辑调整 2025-10-28 19:04:58 +08:00
zdl
356f865f09 feat: 微信mock数据调整 2025-10-28 18:47:39 +08:00
512aca16d8 整合register端口进入login端口 2025-10-28 15:47:50 +08:00
71df2b605b 整合register端口进入login端口 2025-10-28 14:54:45 +08:00
5892dc3156 整合register端口进入login端口 2025-10-28 14:39:37 +08:00
zdl
e05ea154a2 feat: 文案调整 2025-10-28 14:16:30 +08:00
8787d5ddb7 整合register端口进入login端口 2025-10-28 13:45:45 +08:00
zdl
c33181a689 feat: 修复首页新闻中心卡片布局跳变问题
问题根源:
     使用 useBreakpointValue 的 isMobile 变量在初始渲染时返回 undefined,导致:
     1. 服务端渲染/首次加载时显示一种布局
     2. 客户端水合后切换到另一种布局
     3. 用户看到明显的布局跳变(先横向后纵向,或反之)

     解决方案:
     不使用条件渲染两套完全不同的 JSX,而是使用响应式样式让同一套 JSX 自动适应不同屏幕。

     修改策略:
     将移动端(VStack)和桌面端(Flex横向)合并为一套响应式布局:
     - 使用 Flex + 响应式 flexDirection
     - flexDirection={{ base: column, md: row }}(移动端纵向,桌面端横向)
     - 统一使用响应式属性而不是条件渲染
2025-10-28 13:06:46 +08:00
29f035b1cf Merge branch 'feature' of https://git.valuefrontier.cn/vf/vf_react into feature 2025-10-28 11:21:11 +08:00
513134f285 整合register端口进入login端口 2025-10-28 11:20:50 +08:00
zdl
7da50aca40 Merge branch 'feature' of https://git.valuefrontier.cn/vf/vf_react into feature 2025-10-28 11:18:50 +08:00
zdl
72aae585d0 fix: 修复首页路由跳转失败的问题 2025-10-28 11:18:39 +08:00
24c6c9e1c6 修改个股详情中桑基图提示Stack: Error: Sankey is a DAG 2025-10-28 10:46:23 +08:00
zdl
58254d3e8f bugfix:调整 2025-10-27 22:31:41 +08:00
zdl
760ce4d5e1 feat: 路由链接调整 2025-10-27 22:31:06 +08:00
zdl
95c1eaf97b bugfix:修复警告错误 2025-10-27 22:29:53 +08:00
zdl
657c446594 feat: 错误logger 不在被error页面捕获 2025-10-27 21:14:51 +08:00
13 changed files with 593 additions and 404 deletions

View File

@@ -10,7 +10,8 @@
"Bash(npm cache clean --force)",
"Bash(npm install)",
"Bash(npm run start:mock)",
"Bash(npm install fsevents@latest --save-optional --force)"
"Bash(npm install fsevents@latest --save-optional --force)",
"Bash(python -m py_compile:*)"
],
"deny": [],
"ask": []

Binary file not shown.

379
app.py
View File

@@ -1849,6 +1849,15 @@ def send_verification_code():
if not credential or not code_type:
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
# 清理格式字符(空格、横线、括号等)
if code_type == 'phone':
# 移除手机号中的空格、横线、括号、加号等格式字符
credential = re.sub(r'[\s\-\(\)\+]', '', credential)
print(f"📱 清理后的手机号: {credential}")
elif code_type == 'email':
# 邮箱只移除空格
credential = credential.strip()
# 生成验证码
verification_code = generate_verification_code()
@@ -1897,7 +1906,7 @@ def send_verification_code():
@app.route('/api/auth/login-with-code', methods=['POST'])
def login_with_verification_code():
"""使用验证码登录"""
"""使用验证码登录/注册(自动注册)"""
try:
data = request.get_json()
credential = data.get('credential') # 手机号或邮箱
@@ -1907,6 +1916,17 @@ def login_with_verification_code():
if not credential or not verification_code or not login_type:
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
# 清理格式字符(空格、横线、括号等)
if login_type == 'phone':
# 移除手机号中的空格、横线、括号、加号等格式字符
original_credential = credential
credential = re.sub(r'[\s\-\(\)\+]', '', credential)
if original_credential != credential:
print(f"📱 登录时清理手机号: {original_credential} -> {credential}")
elif login_type == 'email':
# 邮箱只移除前后空格
credential = credential.strip()
# 检查验证码
session_key = f'verification_code_{login_type}_{credential}_login'
stored_code_info = session.get(session_key)
@@ -1932,13 +1952,86 @@ def login_with_verification_code():
# 验证码正确,查找用户
user = None
is_new_user = False
if login_type == 'phone':
user = User.query.filter_by(phone=credential).first()
if not user:
# 自动注册新用户
is_new_user = True
# 生成唯一用户名
base_username = f"user_{credential}"
username = base_username
counter = 1
while User.query.filter_by(username=username).first():
username = f"{base_username}_{counter}"
counter += 1
# 创建新用户
user = User(username=username, phone=credential)
user.phone_confirmed = True
user.email = f"{username}@valuefrontier.temp" # 临时邮箱
db.session.add(user)
db.session.commit()
elif login_type == 'email':
user = User.query.filter_by(email=credential).first()
if not user:
# 自动注册新用户
is_new_user = True
# 从邮箱生成用户名
email_prefix = credential.split('@')[0]
base_username = f"user_{email_prefix}"
username = base_username
counter = 1
while User.query.filter_by(username=username).first():
username = f"{base_username}_{counter}"
counter += 1
# 如果用户不存在,自动创建新用户
if not user:
return jsonify({'success': False, 'error': '用户不存在'}), 404
try:
# 生成用户名
if login_type == 'phone':
# 使用手机号生成用户名
base_username = f"用户{credential[-4:]}"
elif login_type == 'email':
# 使用邮箱前缀生成用户名
base_username = credential.split('@')[0]
else:
base_username = "新用户"
# 确保用户名唯一
username = base_username
counter = 1
while User.is_username_taken(username):
username = f"{base_username}_{counter}"
counter += 1
# 创建新用户
user = User(username=username)
# 设置手机号或邮箱
if login_type == 'phone':
user.phone = credential
elif login_type == 'email':
user.email = credential
# 设置默认密码(使用随机密码,用户后续可以修改)
user.set_password(uuid.uuid4().hex)
user.status = 'active'
user.nickname = username
db.session.add(user)
db.session.commit()
is_new_user = True
print(f"✅ 自动创建新用户: {username}, {login_type}: {credential}")
except Exception as e:
print(f"❌ 创建用户失败: {e}")
db.session.rollback()
return jsonify({'success': False, 'error': '创建用户失败'}), 500
# 清除验证码
session.pop(session_key, None)
@@ -1955,9 +2048,13 @@ def login_with_verification_code():
# 更新最后登录时间
user.update_last_seen()
# 根据是否为新用户返回不同的消息
message = '注册成功,欢迎加入!' if is_new_user else '登录成功'
return jsonify({
'success': True,
'message': '登录成功',
'message': message,
'is_new_user': is_new_user,
'user': {
'id': user.id,
'username': user.username,
@@ -1971,6 +2068,7 @@ def login_with_verification_code():
except Exception as e:
print(f"验证码登录错误: {e}")
db.session.rollback()
return jsonify({'success': False, 'error': '登录失败'}), 500
@@ -2023,8 +2121,8 @@ def register():
except Exception as e:
db.session.rollback()
print(f"注册失败: {e}")
return jsonify({'success': False, 'error': '注册失败,请重试'}), 500
print(f"验证码登录/注册错误: {e}")
return jsonify({'success': False, 'error': '登录失败'}), 500
def send_sms_code(phone, code, template_id):
@@ -2504,9 +2602,13 @@ def get_wechat_qrcode():
# 生成唯一state参数
state = uuid.uuid4().hex
print(f"🆕 [QRCODE] 生成新的微信二维码, state={state[:8]}...")
# URL编码回调地址
redirect_uri = urllib.parse.quote_plus(WECHAT_REDIRECT_URI)
print(f"🔗 [QRCODE] 回调地址: {WECHAT_REDIRECT_URI}")
# 构建微信授权URL
wechat_auth_url = (
f"https://open.weixin.qq.com/connect/qrconnect?"
@@ -2524,6 +2626,8 @@ def get_wechat_qrcode():
'wechat_unionid': None
}
print(f"✅ [QRCODE] session 已存储, 当前总数: {len(wechat_qr_sessions)}")
return jsonify({"code":0,
"data":
{
@@ -2587,6 +2691,8 @@ def check_wechat_scan():
del wechat_qr_sessions[session_id]
return jsonify({'status': 'expired'}), 200
print(f"📡 [CHECK] session_id: {session_id[:8]}..., status: {session['status']}, user_info: {session.get('user_info')}")
return jsonify({
'status': session['status'],
'user_info': session.get('user_info'),
@@ -2628,32 +2734,64 @@ def wechat_callback():
state = request.args.get('state')
error = request.args.get('error')
# 错误处理
if error or not code or not state:
# 错误处理:用户拒绝授权
if error:
if state in wechat_qr_sessions:
wechat_qr_sessions[state]['status'] = 'auth_denied'
wechat_qr_sessions[state]['error'] = '用户拒绝授权'
print(f"❌ 用户拒绝授权: state={state}")
return redirect('/auth/signin?error=wechat_auth_denied')
# 参数验证
if not code or not state:
if state in wechat_qr_sessions:
wechat_qr_sessions[state]['status'] = 'auth_failed'
wechat_qr_sessions[state]['error'] = '授权参数缺失'
return redirect('/auth/signin?error=wechat_auth_failed')
# 验证state
if state not in wechat_qr_sessions:
print(f"❌ [CALLBACK] state 不在 wechat_qr_sessions 中: {state[:8]}...")
print(f" 当前 sessions: {list(wechat_qr_sessions.keys())}")
return redirect('/auth/signin?error=session_expired')
session_data = wechat_qr_sessions[state]
print(f"✅ [CALLBACK] 找到 session_data, mode={session_data.get('mode')}")
# 检查过期
if time.time() > session_data['expires']:
print(f"❌ [CALLBACK] session 已过期")
del wechat_qr_sessions[state]
return redirect('/auth/signin?error=session_expired')
try:
# 获取access_token
# 步骤1: 用户已扫码并授权(微信回调过来说明用户已完成扫码+授权)
session_data['status'] = 'scanned'
print(f"✅ 微信扫码回调: state={state}, code={code[:10]}...")
# 步骤2: 获取access_token
token_data = get_wechat_access_token(code)
if not token_data:
session_data['status'] = 'auth_failed'
session_data['error'] = '获取访问令牌失败'
print(f"❌ 获取微信access_token失败: state={state}")
return redirect('/auth/signin?error=token_failed')
# 获取用户信息
# 步骤3: Token获取成功标记为已授权
session_data['status'] = 'authorized'
print(f"✅ 微信授权成功: openid={token_data['openid']}")
# 步骤4: 获取用户信息
user_info = get_wechat_userinfo(token_data['access_token'], token_data['openid'])
if not user_info:
session_data['status'] = 'auth_failed'
session_data['error'] = '获取用户信息失败'
print(f"❌ 获取微信用户信息失败: openid={token_data['openid']}")
return redirect('/auth/signin?error=userinfo_failed')
print(f"✅ [CALLBACK] 获取用户信息成功, nickname={user_info.get('nickname', 'N/A')}")
# 查找或创建用户 / 或处理绑定
openid = token_data['openid']
unionid = user_info.get('unionid') or token_data.get('unionid')
@@ -2696,13 +2834,16 @@ def wechat_callback():
return redirect('/home?bind=failed')
user = None
is_new_user = False
if unionid:
user = User.query.filter_by(wechat_union_id=unionid).first()
if not user:
user = User.query.filter_by(wechat_open_id=openid).first()
if not user:
# 创建新用户
# 创建新用户(自动注册)
is_new_user = True
# 先清理微信昵称
raw_nickname = user_info.get('nickname', '微信用户')
# 创建临时用户实例以使用清理方法
@@ -2726,6 +2867,9 @@ def wechat_callback():
db.session.add(user)
db.session.commit()
is_new_user = True
print(f"✅ 微信扫码自动创建新用户: {username}, openid: {openid}")
# 更新最后登录时间
user.update_last_seen()
@@ -2739,18 +2883,44 @@ def wechat_callback():
# Flask-Login 登录
login_user(user, remember=True)
# 清理微信session(仅登录/注册流程清理;绑定流程在上方已处理,不在此处清理)
# 更新微信session状态,供前端轮询检测
if state in wechat_qr_sessions:
# 仅当不是绑定流程,或没有模式信息时清理
if not wechat_qr_sessions[state].get('mode'):
del wechat_qr_sessions[state]
session_item = wechat_qr_sessions[state]
# 仅处理登录/注册流程,不处理绑定流程
if not session_item.get('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}")
# 直接跳转到首页
return redirect('/home')
# 返回一个简单的成功页面(前端轮询会检测到状态变化)
return '''
<html>
<head><title>授权成功</title></head>
<body>
<h2>微信授权成功</h2>
<p>请返回原页面继续操作...</p>
<script>
// 尝试关闭窗口(如果是弹窗的话)
setTimeout(function() {
window.close();
}, 1000);
</script>
</body>
</html>
''', 200
except Exception as e:
print(f"❌ 微信登录失败: {e}")
import traceback
traceback.print_exc()
db.session.rollback()
# 更新session状态为失败
if state in wechat_qr_sessions:
wechat_qr_sessions[state]['status'] = 'auth_failed'
wechat_qr_sessions[state]['error'] = str(e)
return redirect('/auth/signin?error=login_failed')
@@ -2764,16 +2934,16 @@ def login_with_wechat():
return jsonify({'success': False, 'error': 'session_id不能为空'}), 400
# 验证session
session = wechat_qr_sessions.get(session_id)
if not session:
wechat_session = wechat_qr_sessions.get(session_id)
if not wechat_session:
return jsonify({'success': False, 'error': '会话不存在或已过期'}), 400
# 检查session状态
if session['status'] not in ['login_ready', 'register_ready']:
if wechat_session['status'] not in ['login_success', 'register_success']:
return jsonify({'success': False, 'error': '会话状态无效'}), 400
# 检查是否有用户信息
user_info = session.get('user_info')
user_info = wechat_session.get('user_info')
if not user_info or not user_info.get('user_id'):
return jsonify({'success': False, 'error': '用户信息不完整'}), 400
@@ -2785,18 +2955,33 @@ def login_with_wechat():
# 更新最后登录时间
user.update_last_seen()
# 清除session
# 设置 Flask session
session.permanent = True
session['user_id'] = user.id
session['username'] = user.username
session['logged_in'] = True
session['wechat_login'] = True # 标记是微信登录
# Flask-Login 登录
login_user(user, remember=True)
# 判断是否为新用户
is_new_user = user_info.get('is_new_user', False)
# 清除 wechat_qr_sessions
del wechat_qr_sessions[session_id]
# 生成登录响应
response_data = {
'success': True,
'message': '登录成功' if session['status'] == 'login_ready' else '注册并登录成功',
'message': '注册成功' if is_new_user else '登录成功',
'isNewUser': is_new_user,
'user': {
'id': user.id,
'username': user.username,
'nickname': user.nickname or user.username,
'email': user.email,
'phone': user.phone,
'avatar_url': user.avatar_url,
'has_wechat': True,
'wechat_open_id': user.wechat_open_id,
@@ -2821,61 +3006,6 @@ def login_with_wechat():
}), 500
@app.route('/api/auth/register/wechat', methods=['POST'])
def register_with_wechat():
"""微信注册(保留用于特殊情况)"""
data = request.get_json()
session_id = data.get('session_id')
username = data.get('username')
password = data.get('password')
if not all([session_id, username, password]):
return jsonify({'error': '所有字段都是必填的'}), 400
# 验证session
session = wechat_qr_sessions.get(session_id)
if not session:
return jsonify({'error': '微信验证失败或状态无效'}), 400
if User.query.filter_by(username=username).first():
return jsonify({'error': '用户名已存在'}), 400
# 检查微信OpenID是否已被其他用户使用
wechat_openid = session.get('wechat_openid')
wechat_unionid = session.get('wechat_unionid')
if wechat_unionid and User.query.filter_by(wechat_union_id=wechat_unionid).first():
return jsonify({'error': '该微信号已被其他用户绑定'}), 400
if User.query.filter_by(wechat_open_id=wechat_openid).first():
return jsonify({'error': '该微信号已被其他用户绑定'}), 400
# 创建用户
try:
wechat_info = session['user_info']
user = User(username=username)
user.set_password(password)
# 使用清理后的昵称
user.nickname = user._sanitize_nickname(wechat_info.get('nickname', username))
user.avatar_url = wechat_info.get('avatar_url')
user.wechat_open_id = wechat_openid
user.wechat_union_id = wechat_unionid
db.session.add(user)
db.session.commit()
# 清除session
del wechat_qr_sessions[session_id]
return jsonify({
'message': '注册成功',
'user': user.to_dict()
}), 201
except Exception as e:
db.session.rollback()
print(f"WeChat register error: {e}")
return jsonify({'error': '注册失败,请重试'}), 500
@app.route('/api/account/wechat/unbind', methods=['POST'])
def unbind_wechat_account():
"""解绑当前登录用户的微信"""
@@ -7911,6 +8041,98 @@ def format_date(date_obj):
return str(date_obj)
def remove_cycles_from_sankey_flows(flows_data):
"""
移除Sankey图数据中的循环边确保数据是DAG有向无环图
使用拓扑排序算法检测循环优先保留flow_ratio高的边
Args:
flows_data: list of flow objects with 'source', 'target', 'flow_metrics' keys
Returns:
list of flows without cycles
"""
if not flows_data:
return flows_data
# 按flow_ratio降序排序优先保留重要的边
sorted_flows = sorted(
flows_data,
key=lambda x: x.get('flow_metrics', {}).get('flow_ratio', 0) or 0,
reverse=True
)
# 构建图的邻接表和入度表
def build_graph(flows):
graph = {} # node -> list of successors
in_degree = {} # node -> in-degree count
all_nodes = set()
for flow in flows:
source = flow['source']['node_name']
target = flow['target']['node_name']
all_nodes.add(source)
all_nodes.add(target)
if source not in graph:
graph[source] = []
graph[source].append(target)
if target not in in_degree:
in_degree[target] = 0
in_degree[target] += 1
if source not in in_degree:
in_degree[source] = 0
return graph, in_degree, all_nodes
# 使用Kahn算法检测是否有环
def has_cycle(graph, in_degree, all_nodes):
# 找到所有入度为0的节点
queue = [node for node in all_nodes if in_degree.get(node, 0) == 0]
visited_count = 0
while queue:
node = queue.pop(0)
visited_count += 1
# 访问所有邻居
for neighbor in graph.get(node, []):
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
# 如果访问的节点数等于总节点数,说明没有环
return visited_count < len(all_nodes)
# 逐个添加边,如果添加后产生环则跳过
result_flows = []
for flow in sorted_flows:
# 尝试添加这条边
temp_flows = result_flows + [flow]
# 检查是否产生环
graph, in_degree, all_nodes = build_graph(temp_flows)
# 复制in_degree用于检测因为检测过程会修改它
in_degree_copy = in_degree.copy()
if not has_cycle(graph, in_degree_copy, all_nodes):
# 没有产生环,可以添加
result_flows.append(flow)
else:
# 产生环,跳过这条边
print(f"Skipping edge that creates cycle: {flow['source']['node_name']} -> {flow['target']['node_name']}")
removed_count = len(flows_data) - len(result_flows)
if removed_count > 0:
print(f"Removed {removed_count} edges to eliminate cycles in Sankey diagram")
return result_flows
def get_report_type(date_str):
"""获取报告期类型"""
if not date_str:
@@ -10621,6 +10843,9 @@ def get_value_chain_analysis(company_code):
}
})
# 移除循环边确保Sankey图数据是DAG有向无环图
flows_data = remove_cycles_from_sankey_flows(flows_data)
# 统计各层级节点数量
level_stats = {}
for level_key, nodes in nodes_by_level.items():

View File

@@ -143,7 +143,10 @@ export default function AuthFormContent() {
return;
}
if (!/^1[3-9]\d{9}$/.test(credential)) {
// 清理手机号格式字符(空格、横线、括号等)
const cleanedCredential = credential.replace(/[\s\-\(\)\+]/g, '');
if (!/^1[3-9]\d{9}$/.test(cleanedCredential)) {
toast({
title: "请输入有效的手机号",
status: "warning",
@@ -156,7 +159,7 @@ export default function AuthFormContent() {
setSendingCode(true);
const requestData = {
credential: credential.trim(), // 添加 trim() 防止空格
credential: cleanedCredential, // 使用清理后的手机号
type: 'phone',
purpose: config.api.purpose
};
@@ -189,13 +192,13 @@ export default function AuthFormContent() {
if (response.ok && data.success) {
// ❌ 移除成功 toast静默处理
logger.info('AuthFormContent', '验证码发送成功', {
credential: credential.substring(0, 3) + '****' + credential.substring(7),
credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7),
dev_code: data.dev_code
});
// ✅ 开发环境下在控制台显示验证码
if (data.dev_code) {
console.log(`%c✅ [验证码] ${credential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
console.log(`%c✅ [验证码] ${cleanedCredential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
}
setVerificationCodeSent(true);
@@ -205,7 +208,7 @@ export default function AuthFormContent() {
}
} catch (error) {
logger.api.error('POST', '/api/auth/send-verification-code', error, {
credential: credential.substring(0, 3) + '****' + credential.substring(7)
credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7)
});
// ✅ 显示错误提示给用户
@@ -247,7 +250,10 @@ export default function AuthFormContent() {
return;
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
// 清理手机号格式字符(空格、横线、括号等)
const cleanedPhone = phone.replace(/[\s\-\(\)\+]/g, '');
if (!/^1[3-9]\d{9}$/.test(cleanedPhone)) {
toast({
title: "请输入有效的手机号",
status: "warning",
@@ -258,13 +264,13 @@ export default function AuthFormContent() {
// 构建请求体
const requestBody = {
credential: phone.trim(), // 添加 trim() 防止空格
credential: cleanedPhone, // 使用清理后的手机号
verification_code: verificationCode.trim(), // 添加 trim() 防止空格
login_type: 'phone',
};
logger.api.request('POST', '/api/auth/login-with-code', {
credential: phone.substring(0, 3) + '****' + phone.substring(7),
credential: cleanedPhone.substring(0, 3) + '****' + cleanedPhone.substring(7),
verification_code: verificationCode.substring(0, 2) + '****',
login_type: 'phone'
});

View File

@@ -15,6 +15,8 @@ import { FaQrcode } from "react-icons/fa";
import { FiAlertCircle } from "react-icons/fi";
import { useNavigate } from "react-router-dom";
import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService";
import { useAuthModal } from "../../contexts/AuthModalContext";
import { useAuth } from "../../contexts/AuthContext";
import { logger } from "../../utils/logger";
// 配置常量
@@ -33,6 +35,8 @@ const getStatusColor = (status) => {
case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字
case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字
case WECHAT_STATUS.REGISTER_SUCCESS: return "green.600";
case WECHAT_STATUS.AUTH_DENIED: return "red.600"; // ✅ 红色文字
case WECHAT_STATUS.AUTH_FAILED: return "red.600"; // ✅ 红色文字
default: return "gray.600";
}
};
@@ -45,6 +49,10 @@ const getStatusText = (status) => {
};
export default function WechatRegister() {
// 获取关闭弹窗方法
const { closeModal } = useAuthModal();
const { refreshSession } = useAuth();
// 状态管理
const [wechatAuthUrl, setWechatAuthUrl] = useState("");
const [wechatSessionId, setWechatSessionId] = useState("");
@@ -58,6 +66,7 @@ export default function WechatRegister() {
const timeoutRef = useRef(null);
const isMountedRef = useRef(true); // 追踪组件挂载状态
const containerRef = useRef(null); // 容器DOM引用
const sessionIdRef = useRef(null); // 存储最新的 sessionId避免闭包陷阱
const navigate = useNavigate();
const toast = useToast();
@@ -90,6 +99,7 @@ export default function WechatRegister() {
/**
* 清理所有定时器
* 注意:不清理 sessionIdRef因为 startPolling 时也会调用此函数
*/
const clearTimers = useCallback(() => {
if (pollIntervalRef.current) {
@@ -111,8 +121,12 @@ 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) {
// Session cookie 会自动管理,不需要手动存储
// 如果后端返回了 token可以选择性存储兼容旧方式
@@ -124,14 +138,14 @@ export default function WechatRegister() {
}
showSuccess(
status === WECHAT_STATUS.LOGIN_SUCCESS ? "登录成功" : "注册成功",
"正在跳转..."
status === WECHAT_STATUS.LOGIN_SUCCESS ? "登录成功" : "欢迎回来!"
);
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
navigate("/home");
}, 1000);
// 刷新 AuthContext 状态
await refreshSession();
// 关闭认证弹窗,留在当前页面
closeModal();
} else {
throw new Error(response?.error || '登录失败');
}
@@ -139,17 +153,27 @@ export default function WechatRegister() {
logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId });
showError("登录失败", error.message || "请重试");
}
}, [navigate, showSuccess, showError]);
}, [showSuccess, showError, closeModal, refreshSession]);
/**
* 检查微信扫码状态
* 使用 sessionIdRef.current 避免闭包陷阱
*/
const checkWechatStatus = useCallback(async () => {
// 检查组件是否已卸载
if (!isMountedRef.current || !wechatSessionId) return;
// 检查组件是否已卸载,使用 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(wechatSessionId);
const response = await authService.checkWechatStatus(currentSessionId);
// 安全检查:确保 response 存在且包含 status
if (!response || typeof response.status === 'undefined') {
@@ -158,6 +182,13 @@ export default function WechatRegister() {
}
const { status } = response;
logger.debug('WechatRegister', '微信状态', { status });
logger.debug('WechatRegister', '检测到微信状态', {
sessionId: wechatSessionId.substring(0, 8) + '...',
status,
userInfo: response.user_info
});
// 组件卸载后不再更新状态
if (!isMountedRef.current) return;
@@ -166,24 +197,16 @@ export default function WechatRegister() {
// 处理成功状态
if (status === WECHAT_STATUS.LOGIN_SUCCESS || status === WECHAT_STATUS.REGISTER_SUCCESS) {
logger.info('WechatRegister', '检测到登录成功状态,停止轮询', { status });
clearTimers(); // 停止轮询
sessionIdRef.current = null; // 清理 sessionId
// 显示"扫码成功,登录中"提示
if (isMountedRef.current) {
toast({
title: "扫码成功",
description: "正在登录,请稍候...",
status: "info",
duration: 2000,
isClosable: false,
});
}
await handleLoginSuccess(wechatSessionId, status);
await handleLoginSuccess(currentSessionId, status);
}
// 处理过期状态
else if (status === WECHAT_STATUS.EXPIRED) {
clearTimers();
sessionIdRef.current = null; // 清理 sessionId
if (isMountedRef.current) {
toast({
title: "授权已过期",
@@ -194,12 +217,40 @@ export default function WechatRegister() {
});
}
}
// 处理用户拒绝授权
else if (status === WECHAT_STATUS.AUTH_DENIED) {
clearTimers();
if (isMountedRef.current) {
toast({
title: "授权已取消",
description: "您已取消微信授权登录",
status: "warning",
duration: 3000,
isClosable: true,
});
}
}
// 处理授权失败
else if (status === WECHAT_STATUS.AUTH_FAILED) {
clearTimers();
if (isMountedRef.current) {
const errorMsg = response.error || "授权过程出现错误";
toast({
title: "授权失败",
description: errorMsg,
status: "error",
duration: 5000,
isClosable: true,
});
}
}
} catch (error) {
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: wechatSessionId });
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId });
// 轮询过程中的错误不显示给用户,避免频繁提示
// 但如果错误持续发生,停止轮询避免无限重试
if (error.message.includes('网络连接失败')) {
clearTimers();
sessionIdRef.current = null; // 清理 sessionId
if (isMountedRef.current) {
toast({
title: "网络连接失败",
@@ -211,12 +262,17 @@ export default function WechatRegister() {
}
}
}
}, [wechatSessionId, handleLoginSuccess, clearTimers, toast]);
}, [handleLoginSuccess, clearTimers, toast]);
/**
* 启动轮询
*/
const startPolling = useCallback(() => {
logger.debug('WechatRegister', '启动轮询', {
sessionId: sessionIdRef.current,
interval: POLL_INTERVAL
});
// 清理旧的定时器
clearTimers();
@@ -227,7 +283,9 @@ export default function WechatRegister() {
// 设置超时
timeoutRef.current = setTimeout(() => {
logger.debug('WechatRegister', '二维码超时');
clearTimers();
sessionIdRef.current = null; // 清理 sessionId
setWechatStatus(WECHAT_STATUS.EXPIRED);
}, QR_CODE_TIMEOUT);
}, [checkWechatStatus, clearTimers]);
@@ -254,10 +312,17 @@ export default function WechatRegister() {
throw new Error(response.message || '获取二维码失败');
}
// 同时更新 ref 和 state确保轮询能立即读取到最新值
sessionIdRef.current = response.data.session_id;
setWechatAuthUrl(response.data.auth_url);
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) {
@@ -293,43 +358,10 @@ export default function WechatRegister() {
return () => {
isMountedRef.current = false;
clearTimers();
sessionIdRef.current = null; // 清理 sessionId
};
}, [clearTimers]);
/**
* 备用轮询机制 - 防止丢失状态
* 每3秒检查一次仅在获取到二维码URL且状态为waiting时执行
*/
useEffect(() => {
// 只在有auth_url、session_id且状态为waiting时启动备用轮询
if (wechatAuthUrl && wechatSessionId && wechatStatus === WECHAT_STATUS.WAITING) {
logger.debug('WechatRegister', '备用轮询:启动备用轮询机制');
backupPollIntervalRef.current = setInterval(() => {
try {
if (wechatStatus === WECHAT_STATUS.WAITING && isMountedRef.current) {
logger.debug('WechatRegister', '备用轮询:检查微信状态');
// 添加 .catch() 静默处理异步错误,防止被 ErrorBoundary 捕获
checkWechatStatus().catch(error => {
logger.warn('WechatRegister', '备用轮询检查失败(静默处理)', { error: error.message });
});
}
} catch (error) {
// 捕获所有同步错误,防止被 ErrorBoundary 捕获
logger.warn('WechatRegister', '备用轮询执行出错(静默处理)', { error: error.message });
}
}, BACKUP_POLL_INTERVAL);
}
// 清理备用轮询
return () => {
if (backupPollIntervalRef.current) {
clearInterval(backupPollIntervalRef.current);
backupPollIntervalRef.current = null;
}
};
}, [wechatAuthUrl, wechatSessionId, wechatStatus, checkWechatStatus]);
/**
* 测量容器尺寸并计算缩放比例
*/
@@ -397,7 +429,7 @@ export default function WechatRegister() {
textAlign="center"
mb={3} // 12px底部间距
>
微信扫码
微信登陆
</Heading>
{/* ========== 二维码区域 ========== */}
@@ -414,19 +446,26 @@ export default function WechatRegister() {
bg="gray.50"
boxShadow="sm" // ✅ 添加轻微阴影
>
{wechatStatus === WECHAT_STATUS.WAITING ? (
{wechatStatus !== WECHAT_STATUS.NONE ? (
/* 已获取二维码显示iframe */
<iframe
src={wechatAuthUrl}
title="微信扫码登录"
width="300"
height="350"
scrolling="no" // ✅ 新增:禁止滚动
style={{
border: 'none',
transform: 'scale(0.77) translateY(-20px)', // ✅ 裁剪顶部logo
transform: 'scale(0.77) translateY(-35px)', // ✅ 裁剪顶部logo
transformOrigin: 'top left',
marginLeft: '-5px'
marginLeft: '-5px',
pointerEvents: 'auto', // 允许点击 │ │
overflow: 'hidden', // 尝试隐藏滚动条(可能不起作用)
}}
// 使用 onWheel 事件阻止滚动 │ │
onWheel={(e) => e.preventDefault()} // ✅ 在父容器上阻止滚动
onTouchMove={(e) => e.preventDefault()} // ✅ 移动端也阻止
/>
) : (
/* 未获取:显示占位符 */

View File

@@ -1,14 +1,14 @@
import React from 'react';
import {
Box,
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
Button,
VStack,
Container
} from '@chakra-ui/react';
// import {
// Box,
// Alert,
// AlertIcon,
// AlertTitle,
// AlertDescription,
// Button,
// VStack,
// Container
// } from '@chakra-ui/react';
import { logger } from '../utils/logger';
class ErrorBoundary extends React.Component {
@@ -40,66 +40,68 @@ class ErrorBoundary extends React.Component {
}
render() {
// 如果有错误,显示错误边界(所有环境
if (this.state.hasError) {
return (
<Container maxW="lg" py={20}>
<VStack spacing={6}>
<Alert status="error" borderRadius="lg" p={6}>
<AlertIcon boxSize="24px" />
<Box>
<AlertTitle fontSize="lg" mb={2}>
页面出现错误
</AlertTitle>
<AlertDescription>
{process.env.NODE_ENV === 'development'
? '组件渲染时发生错误,请查看下方详情和控制台日志。'
: '页面加载时发生了未预期的错误,请尝试刷新页面。'}
</AlertDescription>
</Box>
</Alert>
// 静默模式:捕获错误并记录日志(已在 componentDidCatch 中完成
// 但继续渲染子组件,不显示错误页面
// 注意:如果组件因错误无法渲染,该区域可能显示为空白
// // 如果有错误,显示错误边界(所有环境)
// if (this.state.hasError) {
// return (
// <Container maxW="lg" py={20}>
// <VStack spacing={6}>
// <Alert status="error" borderRadius="lg" p={6}>
// <AlertIcon boxSize="24px" />
// <Box>
// <AlertTitle fontSize="lg" mb={2}>
// 页面出现错误!
// </AlertTitle>
// <AlertDescription>
// {process.env.NODE_ENV === 'development'
// ? '组件渲染时发生错误,请查看下方详情和控制台日志。'
// : '页面加载时发生了未预期的错误,请尝试刷新页面。'}
// </AlertDescription>
// </Box>
// </Alert>
{/* 开发环境显示详细错误信息 */}
{process.env.NODE_ENV === 'development' && this.state.error && (
<Box
w="100%"
bg="red.50"
p={4}
borderRadius="lg"
fontSize="sm"
overflow="auto"
maxH="400px"
border="1px"
borderColor="red.200"
>
<Box fontWeight="bold" mb={2} color="red.700">错误详情:</Box>
<Box as="pre" whiteSpace="pre-wrap" color="red.900" fontSize="xs">
<Box fontWeight="bold" mb={1}>{this.state.error.name}: {this.state.error.message}</Box>
{this.state.error.stack && (
<Box mt={2} color="gray.700">{this.state.error.stack}</Box>
)}
{this.state.errorInfo && this.state.errorInfo.componentStack && (
<>
<Box fontWeight="bold" mt={3} mb={1} color="red.700">组件堆栈:</Box>
<Box color="gray.700">{this.state.errorInfo.componentStack}</Box>
</>
)}
</Box>
</Box>
)}
<Button
colorScheme="blue"
size="lg"
onClick={() => window.location.reload()}
>
重新加载页面
</Button>
</VStack>
</Container>
);
}
// {/* 开发环境显示详细错误信息 */}
// {process.env.NODE_ENV === 'development' && this.state.error && (
// <Box
// w="100%"
// bg="red.50"
// p={4}
// borderRadius="lg"
// fontSize="sm"
// overflow="auto"
// maxH="400px"
// border="1px"
// borderColor="red.200"
// >
// <Box fontWeight="bold" mb={2} color="red.700">错误详情:</Box>
// <Box as="pre" whiteSpace="pre-wrap" color="red.900" fontSize="xs">
// <Box fontWeight="bold" mb={1}>{this.state.error.name}: {this.state.error.message}</Box>
// {this.state.error.stack && (
// <Box mt={2} color="gray.700">{this.state.error.stack}</Box>
// )}
// {this.state.errorInfo && this.state.errorInfo.componentStack && (
// <>
// <Box fontWeight="bold" mt={3} mb={1} color="red.700">组件堆栈:</Box>
// <Box color="gray.700">{this.state.errorInfo.componentStack}</Box>
// </>
// )}
// </Box>
// </Box>
// )}
// <Button
// colorScheme="blue"
// size="lg"
// onClick={() => window.location.reload()}
// >
// 重新加载页面
// </Button>
// </VStack>
// </Container>
// );
// }
return this.props.children;
}
}

View File

@@ -212,59 +212,6 @@ export const AuthProvider = ({ children }) => {
}
};
// 注册方法
const register = async (username, email, password) => {
try {
setIsLoading(true);
const formData = new URLSearchParams();
formData.append('username', username);
formData.append('email', email);
formData.append('password', password);
const response = await fetch(`/api/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
credentials: 'include',
body: formData
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || '注册失败');
}
// 注册成功后自动登录
setUser(data.user);
setIsAuthenticated(true);
toast({
title: "注册成功",
description: "欢迎加入价值前沿!",
status: "success",
duration: 3000,
isClosable: true,
});
// ⚡ 注册成功后显示欢迎引导延迟2秒
setTimeout(() => {
showWelcomeGuide();
}, 2000);
return { success: true };
} catch (error) {
logger.error('AuthContext', 'register', error);
// ❌ 移除错误 toast静默失败
return { success: false, error: error.message };
} finally{
setIsLoading(false);
}
};
// 手机号注册
const registerWithPhone = async (phone, code, username, password) => {
@@ -475,7 +422,6 @@ export const AuthProvider = ({ children }) => {
isLoading,
updateUser,
login,
register,
registerWithPhone,
registerWithEmail,
sendSmsCode,

View File

@@ -136,7 +136,9 @@ export const authHandlers = [
});
// 模拟微信授权 URL实际是微信的 URL
const authUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=mock&redirect_uri=&response_type=code&scope=snsapi_login&state=${sessionId}#wechat_redirect`;
// 使用真实的微信 AppID 和真实的授权回调地址(必须与微信开放平台配置的域名一致)
const mockRedirectUri = encodeURIComponent('http://valuefrontier.cn/api/auth/wechat/callback');
const authUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=wxa8d74c47041b5f87&redirect_uri=${mockRedirectUri}&response_type=code&scope=snsapi_login&state=${sessionId}#wechat_redirect`;
console.log('[Mock] 生成微信二维码:', { sessionId, authUrl });
@@ -147,16 +149,16 @@ export const authHandlers = [
session.status = 'scanned';
console.log(`[Mock] 模拟用户扫码: ${sessionId}`);
// 再过2秒自动确认登录
// 再过5秒自动确认登录(延长时间让用户看到 scanned 状态)
setTimeout(() => {
const session2 = mockWechatSessions.get(sessionId);
if (session2 && session2.status === 'scanned') {
session2.status = 'confirmed';
session2.status = 'authorized'; // ✅ 使用 'authorized' 状态,与后端保持一致
session2.user = {
id: 999,
nickname: '微信用户',
wechat_openid: 'mock_openid_' + sessionId,
avatar_url: 'https://i.pravatar.cc/150?img=99',
avatar_url: 'https://ui-avatars.com/api/?name=微信用户&size=150&background=4299e1&color=fff',
phone: null,
email: null,
has_wechat: true,
@@ -168,6 +170,7 @@ export const authHandlers = [
is_subscription_active: true,
subscription_days_left: 0
};
session2.user_info = { user_id: session2.user.id }; // ✅ 添加 user_info 字段
console.log(`[Mock] 模拟用户确认登录: ${sessionId}`, session2.user);
}
}, 2000);
@@ -185,7 +188,7 @@ export const authHandlers = [
}),
// 4. 检查微信扫码状态
http.post('/api/auth/wechat/check-status', async ({ request }) => {
http.post('/api/auth/wechat/check', async ({ request }) => {
await delay(200); // 轮询请求,延迟短一些
const body = await request.json();
@@ -209,18 +212,16 @@ export const authHandlers = [
console.log('[Mock] 检查微信状态:', { session_id, status: session.status });
// ✅ 返回与后端真实 API 一致的扁平化数据结构
return HttpResponse.json({
code: 0,
message: '成功',
data: {
status: session.status,
user: session.user
}
status: session.status,
user_info: session.user_info,
expires_in: Math.floor((session.createdAt + 5 * 60 * 1000 - Date.now()) / 1000)
});
}),
// 5. 微信登录确认
http.post('/api/auth/wechat/login', async ({ request }) => {
http.post('/api/auth/login/wechat', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
@@ -228,7 +229,7 @@ export const authHandlers = [
const session = mockWechatSessions.get(session_id);
if (!session || session.status !== 'confirmed') {
if (!session || session.status !== 'authorized') { // ✅ 使用 'authorized' 状态,与前端保持一致
return HttpResponse.json({
success: false,
error: '微信登录未确认或已过期'
@@ -386,12 +387,12 @@ if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_MOCK
setTimeout(() => {
const session2 = mockWechatSessions.get(targetSessionId);
if (session2 && session2.status === 'scanned') {
session2.status = 'confirmed';
session2.status = 'authorized'; // ✅ 使用 'authorized' 状态,与自动扫码流程保持一致
session2.user = {
id: 999,
nickname: '微信测试用户',
wechat_openid: 'mock_openid_' + targetSessionId,
avatar_url: 'https://i.pravatar.cc/150?img=99',
avatar_url: 'https://ui-avatars.com/api/?name=微信测试用户&size=150&background=4299e1&color=fff',
phone: null,
email: null,
has_wechat: true,
@@ -402,6 +403,7 @@ if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_MOCK
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);

View File

@@ -144,19 +144,23 @@ export const WECHAT_STATUS = {
WAITING: 'waiting',
SCANNED: 'scanned',
AUTHORIZED: 'authorized',
LOGIN_SUCCESS: 'login_success',
REGISTER_SUCCESS: 'register_success',
LOGIN_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
REGISTER_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
EXPIRED: 'expired',
AUTH_DENIED: 'auth_denied', // 用户拒绝授权
AUTH_FAILED: 'auth_failed', // 授权失败
};
/**
* 状态提示信息映射
*/
export const STATUS_MESSAGES = {
[WECHAT_STATUS.WAITING]: '使用微信扫',
[WECHAT_STATUS.WAITING]: '使用微信扫一扫登陆',
[WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认',
[WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...',
[WECHAT_STATUS.EXPIRED]: '二维码已过期',
[WECHAT_STATUS.AUTH_DENIED]: '用户取消授权',
[WECHAT_STATUS.AUTH_FAILED]: '授权失败,请重试',
};
export default authService;

View File

@@ -480,9 +480,10 @@ export default function MidjourneyHeroSection() {
minH="100vh"
bg="linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #000000 100%)"
overflow="hidden"
pointerEvents="none"
>
{/* 粒子背景 */}
<Box position="absolute" inset={0} zIndex={0}>
<Box position="absolute" inset={0} zIndex={-1} pointerEvents="none">
<Particles
id="tsparticles"
init={particlesInit}
@@ -499,7 +500,7 @@ export default function MidjourneyHeroSection() {
<DataStreams />
{/* 内容容器 */}
<Container maxW="7xl" position="relative" zIndex={20} pt={20} pb={20}>
<Container maxW="7xl" position="relative" zIndex={1} pt={20} pb={20}>
<Grid templateColumns={{ base: '1fr', lg: 'repeat(2, 1fr)' }} gap={12} alignItems="center" minH="70vh">
{/* 左侧文本内容 */}
@@ -776,7 +777,7 @@ export default function MidjourneyHeroSection() {
borderRadius="full"
filter="blur(40px)"
animation="pulse 4s ease-in-out infinite"
animationDelay="2s"
sx={{ animationDelay: '2s' }}
/>
</Box>
</Box>
@@ -793,7 +794,7 @@ export default function MidjourneyHeroSection() {
right={0}
h="128px"
bgGradient="linear(to-t, black, transparent)"
zIndex={10}
zIndex={-1}
/>
{/* 全局样式 */}

View File

@@ -138,9 +138,9 @@ const PopularKeywords = ({ onKeywordClick, keywords: propKeywords }) => {
</span>
{/* 所有标签 */}
{keywords.map((item) => (
{keywords.map((item, index) => (
<Tag
key={item.concept_id}
key={item.concept_id || `keyword-${index}`}
color={getTagColor(item.change_pct)}
style={{
cursor: 'pointer',

View File

@@ -523,7 +523,11 @@ const UnifiedSearchBox = ({
onFocus={onSearchFocus}
options={stockOptions}
placeholder="请输入股票代码/股票名称/相关话题"
onPressEnter={handleMainSearch}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleMainSearch();
}
}}
style={{ flex: 1 }}
size="large"
notFoundContent={inputValue && stockOptions.length === 0 ? "未找到匹配的股票" : null}

View File

@@ -34,7 +34,6 @@ export default function HomePage() {
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 });
const isMobile = useBreakpointValue({ base: true, md: false });
// 保留原有的调试信息
useEffect(() => {
@@ -51,11 +50,11 @@ export default function HomePage() {
const coreFeatures = [
{
id: 'news-catalyst',
title: '新闻催化分析',
title: '新闻中心',
description: '实时新闻事件分析,捕捉市场催化因子',
icon: '📊',
color: 'yellow',
url: 'https://valuefrontier.cn/community',
url: '/community',
badge: '核心',
featured: true
},
@@ -65,7 +64,7 @@ export default function HomePage() {
description: '热门概念与主题投资分析追踪',
icon: '🎯',
color: 'purple',
url: 'https://valuefrontier.cn/concepts',
url: '/concepts',
badge: '热门'
},
{
@@ -74,7 +73,7 @@ export default function HomePage() {
description: '全面的个股基本面信息整合',
icon: '📈',
color: 'blue',
url: 'https://valuefrontier.cn/stocks',
url: '/stocks',
badge: '全面'
},
{
@@ -83,7 +82,7 @@ export default function HomePage() {
description: '涨停板数据深度分析与规律挖掘',
icon: '🚀',
color: 'green',
url: 'https://valuefrontier.cn/limit-analyse',
url: '/limit-analyse',
badge: '精准'
},
{
@@ -92,7 +91,7 @@ export default function HomePage() {
description: '个股全方位分析与投资决策支持',
icon: '🧭',
color: 'orange',
url: 'https://valuefrontier.cn/company?scode=688256',
url: '/company?scode=688256',
badge: '专业'
},
{
@@ -106,15 +105,6 @@ export default function HomePage() {
}
];
// 个人中心配置
// const personalCenter = {
// title: '个人中心',
// description: '账户管理与个人设置',
// icon: '👤',
// color: 'gray',
// url: 'https://valuefrontier.cn/home/center'
// };
// @TODO 如何区分内部链接和外部链接?
const handleProductClick = (url) => {
if (url.startsWith('http')) {
@@ -202,7 +192,7 @@ export default function HomePage() {
</>
)}
<Container maxW="7xl" position="relative" zIndex={2} px={containerPx}>
<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 }}>
@@ -225,7 +215,7 @@ export default function HomePage() {
<Box pb={{ base: 8, md: 12 }}>
<VStack spacing={{ base: 6, md: 8 }}>
{/* 新闻催化分析 - 突出显示 */}
{/* 新闻中心 - 突出显示 */}
<Card
bg="transparent"
border="2px solid"
@@ -247,108 +237,77 @@ export default function HomePage() {
}}
>
<CardBody p={{ base: 6, md: 8 }} position="relative" zIndex={1}>
{isMobile ? (
/* 移动端:垂直布局 */
<VStack spacing={4} align="stretch">
<HStack spacing={4}>
<Box
p={3}
borderRadius="lg"
bg="yellow.400"
color="black"
>
<Text fontSize="2xl">{coreFeatures[0].icon}</Text>
</Box>
<VStack align="start" spacing={1} flex={1}>
<Heading size="lg" color="white">
{/* 响应式布局:移动端纵向,桌面端横向 */}
<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="xs">
<Badge colorScheme="yellow" variant="solid" fontSize={{ base: 'xs', md: 'sm' }}>
{coreFeatures[0].badge}
</Badge>
</VStack>
</HStack>
<Text color="whiteAlpha.800" fontSize="md" lineHeight="tall">
{coreFeatures[0].description}
</Text>
<Button
colorScheme="yellow"
size="md"
borderRadius="full"
fontWeight="bold"
w="100%"
onClick={() => handleProductClick(coreFeatures[0].url)}
minH="44px"
>
进入功能
</Button>
</VStack>
) : (
/* 桌面端:横向布局 */
<Flex align="center" justify="space-between">
<HStack spacing={6}>
<Box
p={4}
borderRadius="xl"
bg="yellow.400"
color="black"
>
<Text fontSize="3xl">{coreFeatures[0].icon}</Text>
</Box>
<VStack align="start" spacing={2}>
<HStack>
<Heading size="xl" color="white">
{coreFeatures[0].title}
</Heading>
<Badge colorScheme="yellow" variant="solid" fontSize="sm">
{coreFeatures[0].badge}
</Badge>
</HStack>
<Text color="whiteAlpha.800" fontSize="lg" maxW="md">
{coreFeatures[0].description}
</Text>
</VStack>
</HStack>
<Button
colorScheme="yellow"
size="lg"
borderRadius="full"
fontWeight="bold"
onClick={() => handleProductClick(coreFeatures[0].url)}
>
进入功能
</Button>
</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].url)}
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' }}
cursor="pointer"
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.url)}
minH={{ base: 'auto', md: '180px' }}
>
<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.url)}
minH={{ base: 'auto', md: '180px' }}
>
<CardBody p={{ base: 5, md: 6 }}>
<VStack spacing={{ base: 3, md: 4 }} align="start" h="100%">
<HStack>