Compare commits
6 Commits
feature_20
...
6a51fc3c88
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a51fc3c88 | ||
|
|
7cca5e73c0 | ||
|
|
112fbbd42d | ||
|
|
3a4dade8ec | ||
|
|
6f81259f8c | ||
|
|
864844a52b |
@@ -10,8 +10,7 @@
|
|||||||
"Bash(npm cache clean --force)",
|
"Bash(npm cache clean --force)",
|
||||||
"Bash(npm install)",
|
"Bash(npm install)",
|
||||||
"Bash(npm run start:mock)",
|
"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": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
Binary file not shown.
318
app.py
318
app.py
@@ -1849,15 +1849,6 @@ def send_verification_code():
|
|||||||
if not credential or not code_type:
|
if not credential or not code_type:
|
||||||
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
|
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()
|
verification_code = generate_verification_code()
|
||||||
|
|
||||||
@@ -1906,7 +1897,7 @@ def send_verification_code():
|
|||||||
|
|
||||||
@app.route('/api/auth/login-with-code', methods=['POST'])
|
@app.route('/api/auth/login-with-code', methods=['POST'])
|
||||||
def login_with_verification_code():
|
def login_with_verification_code():
|
||||||
"""使用验证码登录/注册(自动注册)"""
|
"""使用验证码登录"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
credential = data.get('credential') # 手机号或邮箱
|
credential = data.get('credential') # 手机号或邮箱
|
||||||
@@ -1916,17 +1907,6 @@ def login_with_verification_code():
|
|||||||
if not credential or not verification_code or not login_type:
|
if not credential or not verification_code or not login_type:
|
||||||
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
|
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'
|
session_key = f'verification_code_{login_type}_{credential}_login'
|
||||||
stored_code_info = session.get(session_key)
|
stored_code_info = session.get(session_key)
|
||||||
@@ -1952,86 +1932,13 @@ def login_with_verification_code():
|
|||||||
|
|
||||||
# 验证码正确,查找用户
|
# 验证码正确,查找用户
|
||||||
user = None
|
user = None
|
||||||
is_new_user = False
|
|
||||||
|
|
||||||
if login_type == 'phone':
|
if login_type == 'phone':
|
||||||
user = User.query.filter_by(phone=credential).first()
|
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':
|
elif login_type == 'email':
|
||||||
user = User.query.filter_by(email=credential).first()
|
user = User.query.filter_by(email=credential).first()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
# 自动注册新用户
|
return jsonify({'success': False, 'error': '用户不存在'}), 404
|
||||||
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:
|
|
||||||
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)
|
session.pop(session_key, None)
|
||||||
@@ -2048,13 +1955,9 @@ def login_with_verification_code():
|
|||||||
# 更新最后登录时间
|
# 更新最后登录时间
|
||||||
user.update_last_seen()
|
user.update_last_seen()
|
||||||
|
|
||||||
# 根据是否为新用户返回不同的消息
|
|
||||||
message = '注册成功,欢迎加入!' if is_new_user else '登录成功'
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': message,
|
'message': '登录成功',
|
||||||
'is_new_user': is_new_user,
|
|
||||||
'user': {
|
'user': {
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
@@ -2068,7 +1971,6 @@ def login_with_verification_code():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"验证码登录错误: {e}")
|
print(f"验证码登录错误: {e}")
|
||||||
db.session.rollback()
|
|
||||||
return jsonify({'success': False, 'error': '登录失败'}), 500
|
return jsonify({'success': False, 'error': '登录失败'}), 500
|
||||||
|
|
||||||
|
|
||||||
@@ -2121,8 +2023,8 @@ def register():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
print(f"验证码登录/注册错误: {e}")
|
print(f"注册失败: {e}")
|
||||||
return jsonify({'success': False, 'error': '登录失败'}), 500
|
return jsonify({'success': False, 'error': '注册失败,请重试'}), 500
|
||||||
|
|
||||||
|
|
||||||
def send_sms_code(phone, code, template_id):
|
def send_sms_code(phone, code, template_id):
|
||||||
@@ -2726,19 +2628,8 @@ def wechat_callback():
|
|||||||
state = request.args.get('state')
|
state = request.args.get('state')
|
||||||
error = request.args.get('error')
|
error = request.args.get('error')
|
||||||
|
|
||||||
# 错误处理:用户拒绝授权
|
# 错误处理
|
||||||
if error:
|
if error or not code or not state:
|
||||||
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')
|
return redirect('/auth/signin?error=wechat_auth_failed')
|
||||||
|
|
||||||
# 验证state
|
# 验证state
|
||||||
@@ -2753,28 +2644,14 @@ def wechat_callback():
|
|||||||
return redirect('/auth/signin?error=session_expired')
|
return redirect('/auth/signin?error=session_expired')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 步骤1: 用户已扫码并授权(微信回调过来说明用户已完成扫码+授权)
|
# 获取access_token
|
||||||
session_data['status'] = 'scanned'
|
|
||||||
print(f"✅ 微信扫码回调: state={state}, code={code[:10]}...")
|
|
||||||
|
|
||||||
# 步骤2: 获取access_token
|
|
||||||
token_data = get_wechat_access_token(code)
|
token_data = get_wechat_access_token(code)
|
||||||
if not token_data:
|
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')
|
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'])
|
user_info = get_wechat_userinfo(token_data['access_token'], token_data['openid'])
|
||||||
if not user_info:
|
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')
|
return redirect('/auth/signin?error=userinfo_failed')
|
||||||
|
|
||||||
# 查找或创建用户 / 或处理绑定
|
# 查找或创建用户 / 或处理绑定
|
||||||
@@ -2819,8 +2696,6 @@ def wechat_callback():
|
|||||||
return redirect('/home?bind=failed')
|
return redirect('/home?bind=failed')
|
||||||
|
|
||||||
user = None
|
user = None
|
||||||
is_new_user = False
|
|
||||||
|
|
||||||
if unionid:
|
if unionid:
|
||||||
user = User.query.filter_by(wechat_union_id=unionid).first()
|
user = User.query.filter_by(wechat_union_id=unionid).first()
|
||||||
if not user:
|
if not user:
|
||||||
@@ -2851,9 +2726,6 @@ def wechat_callback():
|
|||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
is_new_user = True
|
|
||||||
print(f"✅ 微信扫码自动创建新用户: {username}, openid: {openid}")
|
|
||||||
|
|
||||||
# 更新最后登录时间
|
# 更新最后登录时间
|
||||||
user.update_last_seen()
|
user.update_last_seen()
|
||||||
|
|
||||||
@@ -2867,30 +2739,18 @@ def wechat_callback():
|
|||||||
# Flask-Login 登录
|
# Flask-Login 登录
|
||||||
login_user(user, remember=True)
|
login_user(user, remember=True)
|
||||||
|
|
||||||
# 更新微信session状态,供前端轮询检测
|
# 清理微信session(仅登录/注册流程清理;绑定流程在上方已处理,不在此处清理)
|
||||||
if state in wechat_qr_sessions:
|
if state in wechat_qr_sessions:
|
||||||
session_item = wechat_qr_sessions[state]
|
# 仅当不是绑定流程,或没有模式信息时清理
|
||||||
# 仅处理登录/注册流程,不处理绑定流程
|
if not wechat_qr_sessions[state].get('mode'):
|
||||||
if not session_item.get('mode'):
|
del wechat_qr_sessions[state]
|
||||||
# 更新状态和用户信息
|
|
||||||
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 redirect('/home')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 微信登录失败: {e}")
|
print(f"❌ 微信登录失败: {e}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
db.session.rollback()
|
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')
|
return redirect('/auth/signin?error=login_failed')
|
||||||
|
|
||||||
|
|
||||||
@@ -2961,6 +2821,61 @@ def login_with_wechat():
|
|||||||
}), 500
|
}), 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'])
|
@app.route('/api/account/wechat/unbind', methods=['POST'])
|
||||||
def unbind_wechat_account():
|
def unbind_wechat_account():
|
||||||
"""解绑当前登录用户的微信"""
|
"""解绑当前登录用户的微信"""
|
||||||
@@ -7996,98 +7911,6 @@ def format_date(date_obj):
|
|||||||
return str(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):
|
def get_report_type(date_str):
|
||||||
"""获取报告期类型"""
|
"""获取报告期类型"""
|
||||||
if not date_str:
|
if not date_str:
|
||||||
@@ -10798,9 +10621,6 @@ def get_value_chain_analysis(company_code):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# 移除循环边,确保Sankey图数据是DAG(有向无环图)
|
|
||||||
flows_data = remove_cycles_from_sankey_flows(flows_data)
|
|
||||||
|
|
||||||
# 统计各层级节点数量
|
# 统计各层级节点数量
|
||||||
level_stats = {}
|
level_stats = {}
|
||||||
for level_key, nodes in nodes_by_level.items():
|
for level_key, nodes in nodes_by_level.items():
|
||||||
|
|||||||
@@ -143,10 +143,7 @@ export default function AuthFormContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理手机号格式字符(空格、横线、括号等)
|
if (!/^1[3-9]\d{9}$/.test(credential)) {
|
||||||
const cleanedCredential = credential.replace(/[\s\-\(\)\+]/g, '');
|
|
||||||
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(cleanedCredential)) {
|
|
||||||
toast({
|
toast({
|
||||||
title: "请输入有效的手机号",
|
title: "请输入有效的手机号",
|
||||||
status: "warning",
|
status: "warning",
|
||||||
@@ -159,7 +156,7 @@ export default function AuthFormContent() {
|
|||||||
setSendingCode(true);
|
setSendingCode(true);
|
||||||
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
credential: cleanedCredential, // 使用清理后的手机号
|
credential: credential.trim(), // 添加 trim() 防止空格
|
||||||
type: 'phone',
|
type: 'phone',
|
||||||
purpose: config.api.purpose
|
purpose: config.api.purpose
|
||||||
};
|
};
|
||||||
@@ -192,13 +189,13 @@ export default function AuthFormContent() {
|
|||||||
if (response.ok && data.success) {
|
if (response.ok && data.success) {
|
||||||
// ❌ 移除成功 toast,静默处理
|
// ❌ 移除成功 toast,静默处理
|
||||||
logger.info('AuthFormContent', '验证码发送成功', {
|
logger.info('AuthFormContent', '验证码发送成功', {
|
||||||
credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7),
|
credential: credential.substring(0, 3) + '****' + credential.substring(7),
|
||||||
dev_code: data.dev_code
|
dev_code: data.dev_code
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ 开发环境下在控制台显示验证码
|
// ✅ 开发环境下在控制台显示验证码
|
||||||
if (data.dev_code) {
|
if (data.dev_code) {
|
||||||
console.log(`%c✅ [验证码] ${cleanedCredential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
|
console.log(`%c✅ [验证码] ${credential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
|
||||||
}
|
}
|
||||||
|
|
||||||
setVerificationCodeSent(true);
|
setVerificationCodeSent(true);
|
||||||
@@ -208,7 +205,7 @@ export default function AuthFormContent() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.api.error('POST', '/api/auth/send-verification-code', error, {
|
logger.api.error('POST', '/api/auth/send-verification-code', error, {
|
||||||
credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7)
|
credential: credential.substring(0, 3) + '****' + credential.substring(7)
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ 显示错误提示给用户
|
// ✅ 显示错误提示给用户
|
||||||
@@ -250,10 +247,7 @@ export default function AuthFormContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理手机号格式字符(空格、横线、括号等)
|
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||||
const cleanedPhone = phone.replace(/[\s\-\(\)\+]/g, '');
|
|
||||||
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(cleanedPhone)) {
|
|
||||||
toast({
|
toast({
|
||||||
title: "请输入有效的手机号",
|
title: "请输入有效的手机号",
|
||||||
status: "warning",
|
status: "warning",
|
||||||
@@ -264,13 +258,13 @@ export default function AuthFormContent() {
|
|||||||
|
|
||||||
// 构建请求体
|
// 构建请求体
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
credential: cleanedPhone, // 使用清理后的手机号
|
credential: phone.trim(), // 添加 trim() 防止空格
|
||||||
verification_code: verificationCode.trim(), // 添加 trim() 防止空格
|
verification_code: verificationCode.trim(), // 添加 trim() 防止空格
|
||||||
login_type: 'phone',
|
login_type: 'phone',
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.api.request('POST', '/api/auth/login-with-code', {
|
logger.api.request('POST', '/api/auth/login-with-code', {
|
||||||
credential: cleanedPhone.substring(0, 3) + '****' + cleanedPhone.substring(7),
|
credential: phone.substring(0, 3) + '****' + phone.substring(7),
|
||||||
verification_code: verificationCode.substring(0, 2) + '****',
|
verification_code: verificationCode.substring(0, 2) + '****',
|
||||||
login_type: 'phone'
|
login_type: 'phone'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import { FaQrcode } from "react-icons/fa";
|
|||||||
import { FiAlertCircle } from "react-icons/fi";
|
import { FiAlertCircle } from "react-icons/fi";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService";
|
import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService";
|
||||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
|
||||||
import { useAuth } from "../../contexts/AuthContext";
|
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
|
|
||||||
// 配置常量
|
// 配置常量
|
||||||
@@ -35,8 +33,6 @@ const getStatusColor = (status) => {
|
|||||||
case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字
|
case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字
|
||||||
case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字
|
case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字
|
||||||
case WECHAT_STATUS.REGISTER_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";
|
default: return "gray.600";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -49,10 +45,6 @@ const getStatusText = (status) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function WechatRegister() {
|
export default function WechatRegister() {
|
||||||
// 获取关闭弹窗方法
|
|
||||||
const { closeModal } = useAuthModal();
|
|
||||||
const { refreshSession } = useAuth();
|
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [wechatAuthUrl, setWechatAuthUrl] = useState("");
|
const [wechatAuthUrl, setWechatAuthUrl] = useState("");
|
||||||
const [wechatSessionId, setWechatSessionId] = useState("");
|
const [wechatSessionId, setWechatSessionId] = useState("");
|
||||||
@@ -66,7 +58,6 @@ export default function WechatRegister() {
|
|||||||
const timeoutRef = useRef(null);
|
const timeoutRef = useRef(null);
|
||||||
const isMountedRef = useRef(true); // 追踪组件挂载状态
|
const isMountedRef = useRef(true); // 追踪组件挂载状态
|
||||||
const containerRef = useRef(null); // 容器DOM引用
|
const containerRef = useRef(null); // 容器DOM引用
|
||||||
const sessionIdRef = useRef(null); // 存储最新的 sessionId,避免闭包陷阱
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -99,7 +90,6 @@ export default function WechatRegister() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理所有定时器
|
* 清理所有定时器
|
||||||
* 注意:不清理 sessionIdRef,因为 startPolling 时也会调用此函数
|
|
||||||
*/
|
*/
|
||||||
const clearTimers = useCallback(() => {
|
const clearTimers = useCallback(() => {
|
||||||
if (pollIntervalRef.current) {
|
if (pollIntervalRef.current) {
|
||||||
@@ -134,14 +124,14 @@ export default function WechatRegister() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showSuccess(
|
showSuccess(
|
||||||
status === WECHAT_STATUS.LOGIN_SUCCESS ? "登录成功" : "欢迎回来!"
|
status === WECHAT_STATUS.LOGIN_SUCCESS ? "登录成功" : "注册成功",
|
||||||
|
"正在跳转..."
|
||||||
);
|
);
|
||||||
|
|
||||||
// 刷新 AuthContext 状态
|
// 延迟跳转,让用户看到成功提示
|
||||||
await refreshSession();
|
setTimeout(() => {
|
||||||
|
navigate("/home");
|
||||||
// 关闭认证弹窗,留在当前页面
|
}, 1000);
|
||||||
closeModal();
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response?.error || '登录失败');
|
throw new Error(response?.error || '登录失败');
|
||||||
}
|
}
|
||||||
@@ -149,27 +139,17 @@ export default function WechatRegister() {
|
|||||||
logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId });
|
logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId });
|
||||||
showError("登录失败", error.message || "请重试");
|
showError("登录失败", error.message || "请重试");
|
||||||
}
|
}
|
||||||
}, [showSuccess, showError, closeModal, refreshSession]);
|
}, [navigate, showSuccess, showError]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查微信扫码状态
|
* 检查微信扫码状态
|
||||||
* 使用 sessionIdRef.current 避免闭包陷阱
|
|
||||||
*/
|
*/
|
||||||
const checkWechatStatus = useCallback(async () => {
|
const checkWechatStatus = useCallback(async () => {
|
||||||
// 检查组件是否已卸载,使用 ref 获取最新的 sessionId
|
// 检查组件是否已卸载
|
||||||
if (!isMountedRef.current || !sessionIdRef.current) {
|
if (!isMountedRef.current || !wechatSessionId) return;
|
||||||
logger.debug('WechatRegister', 'checkWechatStatus 跳过', {
|
|
||||||
isMounted: isMountedRef.current,
|
|
||||||
hasSessionId: !!sessionIdRef.current
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSessionId = sessionIdRef.current;
|
|
||||||
logger.debug('WechatRegister', '检查微信状态', { sessionId: currentSessionId });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authService.checkWechatStatus(currentSessionId);
|
const response = await authService.checkWechatStatus(wechatSessionId);
|
||||||
|
|
||||||
// 安全检查:确保 response 存在且包含 status
|
// 安全检查:确保 response 存在且包含 status
|
||||||
if (!response || typeof response.status === 'undefined') {
|
if (!response || typeof response.status === 'undefined') {
|
||||||
@@ -178,7 +158,6 @@ export default function WechatRegister() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { status } = response;
|
const { status } = response;
|
||||||
logger.debug('WechatRegister', '微信状态', { status });
|
|
||||||
|
|
||||||
// 组件卸载后不再更新状态
|
// 组件卸载后不再更新状态
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
@@ -188,14 +167,23 @@ export default function WechatRegister() {
|
|||||||
// 处理成功状态
|
// 处理成功状态
|
||||||
if (status === WECHAT_STATUS.LOGIN_SUCCESS || status === WECHAT_STATUS.REGISTER_SUCCESS) {
|
if (status === WECHAT_STATUS.LOGIN_SUCCESS || status === WECHAT_STATUS.REGISTER_SUCCESS) {
|
||||||
clearTimers(); // 停止轮询
|
clearTimers(); // 停止轮询
|
||||||
sessionIdRef.current = null; // 清理 sessionId
|
|
||||||
|
|
||||||
await handleLoginSuccess(currentSessionId, status);
|
// 显示"扫码成功,登录中"提示
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
toast({
|
||||||
|
title: "扫码成功",
|
||||||
|
description: "正在登录,请稍候...",
|
||||||
|
status: "info",
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleLoginSuccess(wechatSessionId, status);
|
||||||
}
|
}
|
||||||
// 处理过期状态
|
// 处理过期状态
|
||||||
else if (status === WECHAT_STATUS.EXPIRED) {
|
else if (status === WECHAT_STATUS.EXPIRED) {
|
||||||
clearTimers();
|
clearTimers();
|
||||||
sessionIdRef.current = null; // 清理 sessionId
|
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
toast({
|
toast({
|
||||||
title: "授权已过期",
|
title: "授权已过期",
|
||||||
@@ -206,40 +194,12 @@ 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) {
|
} catch (error) {
|
||||||
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId });
|
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: wechatSessionId });
|
||||||
// 轮询过程中的错误不显示给用户,避免频繁提示
|
// 轮询过程中的错误不显示给用户,避免频繁提示
|
||||||
// 但如果错误持续发生,停止轮询避免无限重试
|
// 但如果错误持续发生,停止轮询避免无限重试
|
||||||
if (error.message.includes('网络连接失败')) {
|
if (error.message.includes('网络连接失败')) {
|
||||||
clearTimers();
|
clearTimers();
|
||||||
sessionIdRef.current = null; // 清理 sessionId
|
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
toast({
|
toast({
|
||||||
title: "网络连接失败",
|
title: "网络连接失败",
|
||||||
@@ -251,17 +211,12 @@ export default function WechatRegister() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [handleLoginSuccess, clearTimers, toast]);
|
}, [wechatSessionId, handleLoginSuccess, clearTimers, toast]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动轮询
|
* 启动轮询
|
||||||
*/
|
*/
|
||||||
const startPolling = useCallback(() => {
|
const startPolling = useCallback(() => {
|
||||||
logger.debug('WechatRegister', '启动轮询', {
|
|
||||||
sessionId: sessionIdRef.current,
|
|
||||||
interval: POLL_INTERVAL
|
|
||||||
});
|
|
||||||
|
|
||||||
// 清理旧的定时器
|
// 清理旧的定时器
|
||||||
clearTimers();
|
clearTimers();
|
||||||
|
|
||||||
@@ -272,9 +227,7 @@ export default function WechatRegister() {
|
|||||||
|
|
||||||
// 设置超时
|
// 设置超时
|
||||||
timeoutRef.current = setTimeout(() => {
|
timeoutRef.current = setTimeout(() => {
|
||||||
logger.debug('WechatRegister', '二维码超时');
|
|
||||||
clearTimers();
|
clearTimers();
|
||||||
sessionIdRef.current = null; // 清理 sessionId
|
|
||||||
setWechatStatus(WECHAT_STATUS.EXPIRED);
|
setWechatStatus(WECHAT_STATUS.EXPIRED);
|
||||||
}, QR_CODE_TIMEOUT);
|
}, QR_CODE_TIMEOUT);
|
||||||
}, [checkWechatStatus, clearTimers]);
|
}, [checkWechatStatus, clearTimers]);
|
||||||
@@ -301,17 +254,10 @@ export default function WechatRegister() {
|
|||||||
throw new Error(response.message || '获取二维码失败');
|
throw new Error(response.message || '获取二维码失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同时更新 ref 和 state,确保轮询能立即读取到最新值
|
|
||||||
sessionIdRef.current = response.data.session_id;
|
|
||||||
setWechatAuthUrl(response.data.auth_url);
|
setWechatAuthUrl(response.data.auth_url);
|
||||||
setWechatSessionId(response.data.session_id);
|
setWechatSessionId(response.data.session_id);
|
||||||
setWechatStatus(WECHAT_STATUS.WAITING);
|
setWechatStatus(WECHAT_STATUS.WAITING);
|
||||||
|
|
||||||
logger.debug('WechatRegister', '获取二维码成功', {
|
|
||||||
sessionId: response.data.session_id,
|
|
||||||
authUrl: response.data.auth_url
|
|
||||||
});
|
|
||||||
|
|
||||||
// 启动轮询检查扫码状态
|
// 启动轮询检查扫码状态
|
||||||
startPolling();
|
startPolling();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -347,10 +293,43 @@ export default function WechatRegister() {
|
|||||||
return () => {
|
return () => {
|
||||||
isMountedRef.current = false;
|
isMountedRef.current = false;
|
||||||
clearTimers();
|
clearTimers();
|
||||||
sessionIdRef.current = null; // 清理 sessionId
|
|
||||||
};
|
};
|
||||||
}, [clearTimers]);
|
}, [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]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测量容器尺寸并计算缩放比例
|
* 测量容器尺寸并计算缩放比例
|
||||||
*/
|
*/
|
||||||
@@ -418,7 +397,7 @@ export default function WechatRegister() {
|
|||||||
textAlign="center"
|
textAlign="center"
|
||||||
mb={3} // 12px底部间距
|
mb={3} // 12px底部间距
|
||||||
>
|
>
|
||||||
微信登陆
|
微信扫码
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
{/* ========== 二维码区域 ========== */}
|
{/* ========== 二维码区域 ========== */}
|
||||||
@@ -435,26 +414,19 @@ export default function WechatRegister() {
|
|||||||
bg="gray.50"
|
bg="gray.50"
|
||||||
boxShadow="sm" // ✅ 添加轻微阴影
|
boxShadow="sm" // ✅ 添加轻微阴影
|
||||||
>
|
>
|
||||||
{wechatStatus !== WECHAT_STATUS.NONE ? (
|
{wechatStatus === WECHAT_STATUS.WAITING ? (
|
||||||
/* 已获取二维码:显示iframe */
|
/* 已获取二维码:显示iframe */
|
||||||
<iframe
|
<iframe
|
||||||
src={wechatAuthUrl}
|
src={wechatAuthUrl}
|
||||||
title="微信扫码登录"
|
title="微信扫码登录"
|
||||||
width="300"
|
width="300"
|
||||||
height="350"
|
height="350"
|
||||||
scrolling="no" // ✅ 新增:禁止滚动
|
|
||||||
style={{
|
style={{
|
||||||
border: 'none',
|
border: 'none',
|
||||||
transform: 'scale(0.77) translateY(-35px)', // ✅ 裁剪顶部logo
|
transform: 'scale(0.77) translateY(-20px)', // ✅ 裁剪顶部logo
|
||||||
transformOrigin: 'top left',
|
transformOrigin: 'top left',
|
||||||
marginLeft: '-5px',
|
marginLeft: '-5px'
|
||||||
pointerEvents: 'auto', // 允许点击 │ │
|
|
||||||
overflow: 'hidden', // 尝试隐藏滚动条(可能不起作用)
|
|
||||||
}}
|
}}
|
||||||
// 使用 onWheel 事件阻止滚动 │ │
|
|
||||||
onWheel={(e) => e.preventDefault()} // ✅ 在父容器上阻止滚动
|
|
||||||
onTouchMove={(e) => e.preventDefault()} // ✅ 移动端也阻止
|
|
||||||
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
/* 未获取:显示占位符 */
|
/* 未获取:显示占位符 */
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
// import {
|
import {
|
||||||
// Box,
|
Box,
|
||||||
// Alert,
|
Alert,
|
||||||
// AlertIcon,
|
AlertIcon,
|
||||||
// AlertTitle,
|
AlertTitle,
|
||||||
// AlertDescription,
|
AlertDescription,
|
||||||
// Button,
|
Button,
|
||||||
// VStack,
|
VStack,
|
||||||
// Container
|
Container
|
||||||
// } from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
class ErrorBoundary extends React.Component {
|
class ErrorBoundary extends React.Component {
|
||||||
@@ -40,68 +40,66 @@ class ErrorBoundary extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// 静默模式:捕获错误并记录日志(已在 componentDidCatch 中完成)
|
// 如果有错误,显示错误边界(所有环境)
|
||||||
// 但继续渲染子组件,不显示错误页面
|
if (this.state.hasError) {
|
||||||
// 注意:如果组件因错误无法渲染,该区域可能显示为空白
|
return (
|
||||||
// // 如果有错误,显示错误边界(所有环境)
|
<Container maxW="lg" py={20}>
|
||||||
// if (this.state.hasError) {
|
<VStack spacing={6}>
|
||||||
// return (
|
<Alert status="error" borderRadius="lg" p={6}>
|
||||||
// <Container maxW="lg" py={20}>
|
<AlertIcon boxSize="24px" />
|
||||||
// <VStack spacing={6}>
|
<Box>
|
||||||
// <Alert status="error" borderRadius="lg" p={6}>
|
<AlertTitle fontSize="lg" mb={2}>
|
||||||
// <AlertIcon boxSize="24px" />
|
页面出现错误!
|
||||||
// <Box>
|
</AlertTitle>
|
||||||
// <AlertTitle fontSize="lg" mb={2}>
|
<AlertDescription>
|
||||||
// 页面出现错误!
|
{process.env.NODE_ENV === 'development'
|
||||||
// </AlertTitle>
|
? '组件渲染时发生错误,请查看下方详情和控制台日志。'
|
||||||
// <AlertDescription>
|
: '页面加载时发生了未预期的错误,请尝试刷新页面。'}
|
||||||
// {process.env.NODE_ENV === 'development'
|
</AlertDescription>
|
||||||
// ? '组件渲染时发生错误,请查看下方详情和控制台日志。'
|
</Box>
|
||||||
// : '页面加载时发生了未预期的错误,请尝试刷新页面。'}
|
</Alert>
|
||||||
// </AlertDescription>
|
|
||||||
// </Box>
|
|
||||||
// </Alert>
|
|
||||||
|
|
||||||
// {/* 开发环境显示详细错误信息 */}
|
{/* 开发环境显示详细错误信息 */}
|
||||||
// {process.env.NODE_ENV === 'development' && this.state.error && (
|
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||||
// <Box
|
<Box
|
||||||
// w="100%"
|
w="100%"
|
||||||
// bg="red.50"
|
bg="red.50"
|
||||||
// p={4}
|
p={4}
|
||||||
// borderRadius="lg"
|
borderRadius="lg"
|
||||||
// fontSize="sm"
|
fontSize="sm"
|
||||||
// overflow="auto"
|
overflow="auto"
|
||||||
// maxH="400px"
|
maxH="400px"
|
||||||
// border="1px"
|
border="1px"
|
||||||
// borderColor="red.200"
|
borderColor="red.200"
|
||||||
// >
|
>
|
||||||
// <Box fontWeight="bold" mb={2} color="red.700">错误详情:</Box>
|
<Box fontWeight="bold" mb={2} color="red.700">错误详情:</Box>
|
||||||
// <Box as="pre" whiteSpace="pre-wrap" color="red.900" fontSize="xs">
|
<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>
|
<Box fontWeight="bold" mb={1}>{this.state.error.name}: {this.state.error.message}</Box>
|
||||||
// {this.state.error.stack && (
|
{this.state.error.stack && (
|
||||||
// <Box mt={2} color="gray.700">{this.state.error.stack}</Box>
|
<Box mt={2} color="gray.700">{this.state.error.stack}</Box>
|
||||||
// )}
|
)}
|
||||||
// {this.state.errorInfo && this.state.errorInfo.componentStack && (
|
{this.state.errorInfo && this.state.errorInfo.componentStack && (
|
||||||
// <>
|
<>
|
||||||
// <Box fontWeight="bold" mt={3} mb={1} color="red.700">组件堆栈:</Box>
|
<Box fontWeight="bold" mt={3} mb={1} color="red.700">组件堆栈:</Box>
|
||||||
// <Box color="gray.700">{this.state.errorInfo.componentStack}</Box>
|
<Box color="gray.700">{this.state.errorInfo.componentStack}</Box>
|
||||||
// </>
|
</>
|
||||||
// )}
|
)}
|
||||||
// </Box>
|
</Box>
|
||||||
// </Box>
|
</Box>
|
||||||
// )}
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
size="lg"
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
>
|
||||||
|
重新加载页面
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// <Button
|
|
||||||
// colorScheme="blue"
|
|
||||||
// size="lg"
|
|
||||||
// onClick={() => window.location.reload()}
|
|
||||||
// >
|
|
||||||
// 重新加载页面
|
|
||||||
// </Button>
|
|
||||||
// </VStack>
|
|
||||||
// </Container>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
return this.props.children;
|
return this.props.children;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,59 @@ 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) => {
|
const registerWithPhone = async (phone, code, username, password) => {
|
||||||
@@ -422,6 +475,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
updateUser,
|
updateUser,
|
||||||
login,
|
login,
|
||||||
|
register,
|
||||||
registerWithPhone,
|
registerWithPhone,
|
||||||
registerWithEmail,
|
registerWithEmail,
|
||||||
sendSmsCode,
|
sendSmsCode,
|
||||||
|
|||||||
@@ -136,9 +136,7 @@ export const authHandlers = [
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 模拟微信授权 URL(实际是微信的 URL)
|
// 模拟微信授权 URL(实际是微信的 URL)
|
||||||
// 使用真实的微信 AppID 和真实的授权回调地址(必须与微信开放平台配置的域名一致)
|
const authUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=mock&redirect_uri=&response_type=code&scope=snsapi_login&state=${sessionId}#wechat_redirect`;
|
||||||
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 });
|
console.log('[Mock] 生成微信二维码:', { sessionId, authUrl });
|
||||||
|
|
||||||
@@ -149,16 +147,16 @@ export const authHandlers = [
|
|||||||
session.status = 'scanned';
|
session.status = 'scanned';
|
||||||
console.log(`[Mock] 模拟用户扫码: ${sessionId}`);
|
console.log(`[Mock] 模拟用户扫码: ${sessionId}`);
|
||||||
|
|
||||||
// 再过5秒自动确认登录(延长时间让用户看到 scanned 状态)
|
// 再过2秒自动确认登录
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const session2 = mockWechatSessions.get(sessionId);
|
const session2 = mockWechatSessions.get(sessionId);
|
||||||
if (session2 && session2.status === 'scanned') {
|
if (session2 && session2.status === 'scanned') {
|
||||||
session2.status = 'authorized'; // ✅ 使用 'authorized' 状态,与后端保持一致
|
session2.status = 'confirmed';
|
||||||
session2.user = {
|
session2.user = {
|
||||||
id: 999,
|
id: 999,
|
||||||
nickname: '微信用户',
|
nickname: '微信用户',
|
||||||
wechat_openid: 'mock_openid_' + sessionId,
|
wechat_openid: 'mock_openid_' + sessionId,
|
||||||
avatar_url: 'https://ui-avatars.com/api/?name=微信用户&size=150&background=4299e1&color=fff',
|
avatar_url: 'https://i.pravatar.cc/150?img=99',
|
||||||
phone: null,
|
phone: null,
|
||||||
email: null,
|
email: null,
|
||||||
has_wechat: true,
|
has_wechat: true,
|
||||||
@@ -170,7 +168,6 @@ export const authHandlers = [
|
|||||||
is_subscription_active: true,
|
is_subscription_active: true,
|
||||||
subscription_days_left: 0
|
subscription_days_left: 0
|
||||||
};
|
};
|
||||||
session2.user_info = { user_id: session2.user.id }; // ✅ 添加 user_info 字段
|
|
||||||
console.log(`[Mock] 模拟用户确认登录: ${sessionId}`, session2.user);
|
console.log(`[Mock] 模拟用户确认登录: ${sessionId}`, session2.user);
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -188,7 +185,7 @@ export const authHandlers = [
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
// 4. 检查微信扫码状态
|
// 4. 检查微信扫码状态
|
||||||
http.post('/api/auth/wechat/check', async ({ request }) => {
|
http.post('/api/auth/wechat/check-status', async ({ request }) => {
|
||||||
await delay(200); // 轮询请求,延迟短一些
|
await delay(200); // 轮询请求,延迟短一些
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
@@ -212,16 +209,18 @@ export const authHandlers = [
|
|||||||
|
|
||||||
console.log('[Mock] 检查微信状态:', { session_id, status: session.status });
|
console.log('[Mock] 检查微信状态:', { session_id, status: session.status });
|
||||||
|
|
||||||
// ✅ 返回与后端真实 API 一致的扁平化数据结构
|
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
|
code: 0,
|
||||||
|
message: '成功',
|
||||||
|
data: {
|
||||||
status: session.status,
|
status: session.status,
|
||||||
user_info: session.user_info,
|
user: session.user
|
||||||
expires_in: Math.floor((session.createdAt + 5 * 60 * 1000 - Date.now()) / 1000)
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 5. 微信登录确认
|
// 5. 微信登录确认
|
||||||
http.post('/api/auth/login/wechat', async ({ request }) => {
|
http.post('/api/auth/wechat/login', async ({ request }) => {
|
||||||
await delay(NETWORK_DELAY);
|
await delay(NETWORK_DELAY);
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
@@ -229,7 +228,7 @@ export const authHandlers = [
|
|||||||
|
|
||||||
const session = mockWechatSessions.get(session_id);
|
const session = mockWechatSessions.get(session_id);
|
||||||
|
|
||||||
if (!session || session.status !== 'authorized') { // ✅ 使用 'authorized' 状态,与前端保持一致
|
if (!session || session.status !== 'confirmed') {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: '微信登录未确认或已过期'
|
error: '微信登录未确认或已过期'
|
||||||
@@ -387,12 +386,12 @@ if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_MOCK
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const session2 = mockWechatSessions.get(targetSessionId);
|
const session2 = mockWechatSessions.get(targetSessionId);
|
||||||
if (session2 && session2.status === 'scanned') {
|
if (session2 && session2.status === 'scanned') {
|
||||||
session2.status = 'authorized'; // ✅ 使用 'authorized' 状态,与自动扫码流程保持一致
|
session2.status = 'confirmed';
|
||||||
session2.user = {
|
session2.user = {
|
||||||
id: 999,
|
id: 999,
|
||||||
nickname: '微信测试用户',
|
nickname: '微信测试用户',
|
||||||
wechat_openid: 'mock_openid_' + targetSessionId,
|
wechat_openid: 'mock_openid_' + targetSessionId,
|
||||||
avatar_url: 'https://ui-avatars.com/api/?name=微信测试用户&size=150&background=4299e1&color=fff',
|
avatar_url: 'https://i.pravatar.cc/150?img=99',
|
||||||
phone: null,
|
phone: null,
|
||||||
email: null,
|
email: null,
|
||||||
has_wechat: true,
|
has_wechat: true,
|
||||||
@@ -403,7 +402,6 @@ if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_MOCK
|
|||||||
is_subscription_active: true,
|
is_subscription_active: true,
|
||||||
subscription_days_left: 0
|
subscription_days_left: 0
|
||||||
};
|
};
|
||||||
session2.user_info = { user_id: session2.user.id }; // ✅ 添加 user_info 字段
|
|
||||||
console.log(`[Mock API] ✅ 模拟确认登录: ${targetSessionId}`, session2.user);
|
console.log(`[Mock API] ✅ 模拟确认登录: ${targetSessionId}`, session2.user);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|||||||
@@ -144,23 +144,19 @@ export const WECHAT_STATUS = {
|
|||||||
WAITING: 'waiting',
|
WAITING: 'waiting',
|
||||||
SCANNED: 'scanned',
|
SCANNED: 'scanned',
|
||||||
AUTHORIZED: 'authorized',
|
AUTHORIZED: 'authorized',
|
||||||
LOGIN_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
|
LOGIN_SUCCESS: 'login_success',
|
||||||
REGISTER_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
|
REGISTER_SUCCESS: 'register_success',
|
||||||
EXPIRED: 'expired',
|
EXPIRED: 'expired',
|
||||||
AUTH_DENIED: 'auth_denied', // 用户拒绝授权
|
|
||||||
AUTH_FAILED: 'auth_failed', // 授权失败
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态提示信息映射
|
* 状态提示信息映射
|
||||||
*/
|
*/
|
||||||
export const STATUS_MESSAGES = {
|
export const STATUS_MESSAGES = {
|
||||||
[WECHAT_STATUS.WAITING]: '使用微信扫一扫登陆',
|
[WECHAT_STATUS.WAITING]: '请使用微信扫码',
|
||||||
[WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认',
|
[WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认',
|
||||||
[WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...',
|
[WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...',
|
||||||
[WECHAT_STATUS.EXPIRED]: '二维码已过期',
|
[WECHAT_STATUS.EXPIRED]: '二维码已过期',
|
||||||
[WECHAT_STATUS.AUTH_DENIED]: '用户取消授权',
|
|
||||||
[WECHAT_STATUS.AUTH_FAILED]: '授权失败,请重试',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default authService;
|
export default authService;
|
||||||
|
|||||||
@@ -480,10 +480,9 @@ export default function MidjourneyHeroSection() {
|
|||||||
minH="100vh"
|
minH="100vh"
|
||||||
bg="linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #000000 100%)"
|
bg="linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #000000 100%)"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
pointerEvents="none"
|
|
||||||
>
|
>
|
||||||
{/* 粒子背景 */}
|
{/* 粒子背景 */}
|
||||||
<Box position="absolute" inset={0} zIndex={-1} pointerEvents="none">
|
<Box position="absolute" inset={0} zIndex={0}>
|
||||||
<Particles
|
<Particles
|
||||||
id="tsparticles"
|
id="tsparticles"
|
||||||
init={particlesInit}
|
init={particlesInit}
|
||||||
@@ -500,7 +499,7 @@ export default function MidjourneyHeroSection() {
|
|||||||
<DataStreams />
|
<DataStreams />
|
||||||
|
|
||||||
{/* 内容容器 */}
|
{/* 内容容器 */}
|
||||||
<Container maxW="7xl" position="relative" zIndex={1} pt={20} pb={20}>
|
<Container maxW="7xl" position="relative" zIndex={20} pt={20} pb={20}>
|
||||||
<Grid templateColumns={{ base: '1fr', lg: 'repeat(2, 1fr)' }} gap={12} alignItems="center" minH="70vh">
|
<Grid templateColumns={{ base: '1fr', lg: 'repeat(2, 1fr)' }} gap={12} alignItems="center" minH="70vh">
|
||||||
|
|
||||||
{/* 左侧文本内容 */}
|
{/* 左侧文本内容 */}
|
||||||
@@ -777,7 +776,7 @@ export default function MidjourneyHeroSection() {
|
|||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
filter="blur(40px)"
|
filter="blur(40px)"
|
||||||
animation="pulse 4s ease-in-out infinite"
|
animation="pulse 4s ease-in-out infinite"
|
||||||
sx={{ animationDelay: '2s' }}
|
animationDelay="2s"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -794,7 +793,7 @@ export default function MidjourneyHeroSection() {
|
|||||||
right={0}
|
right={0}
|
||||||
h="128px"
|
h="128px"
|
||||||
bgGradient="linear(to-t, black, transparent)"
|
bgGradient="linear(to-t, black, transparent)"
|
||||||
zIndex={-1}
|
zIndex={10}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 全局样式 */}
|
{/* 全局样式 */}
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ const PopularKeywords = ({ onKeywordClick, keywords: propKeywords }) => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* 所有标签 */}
|
{/* 所有标签 */}
|
||||||
{keywords.map((item, index) => (
|
{keywords.map((item) => (
|
||||||
<Tag
|
<Tag
|
||||||
key={item.concept_id || `keyword-${index}`}
|
key={item.concept_id}
|
||||||
color={getTagColor(item.change_pct)}
|
color={getTagColor(item.change_pct)}
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|||||||
@@ -523,11 +523,7 @@ const UnifiedSearchBox = ({
|
|||||||
onFocus={onSearchFocus}
|
onFocus={onSearchFocus}
|
||||||
options={stockOptions}
|
options={stockOptions}
|
||||||
placeholder="请输入股票代码/股票名称/相关话题"
|
placeholder="请输入股票代码/股票名称/相关话题"
|
||||||
onKeyDown={(e) => {
|
onPressEnter={handleMainSearch}
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleMainSearch();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
size="large"
|
size="large"
|
||||||
notFoundContent={inputValue && stockOptions.length === 0 ? "未找到匹配的股票" : null}
|
notFoundContent={inputValue && stockOptions.length === 0 ? "未找到匹配的股票" : null}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export default function HomePage() {
|
|||||||
const heroTextSize = useBreakpointValue({ base: 'md', md: 'lg', lg: 'xl' });
|
const heroTextSize = useBreakpointValue({ base: 'md', md: 'lg', lg: 'xl' });
|
||||||
const containerPx = useBreakpointValue({ base: 10, md: 10, lg: 10 });
|
const containerPx = useBreakpointValue({ base: 10, md: 10, lg: 10 });
|
||||||
const showDecorations = useBreakpointValue({ base: false, md: true });
|
const showDecorations = useBreakpointValue({ base: false, md: true });
|
||||||
|
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||||
|
|
||||||
// 保留原有的调试信息
|
// 保留原有的调试信息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -50,11 +51,11 @@ export default function HomePage() {
|
|||||||
const coreFeatures = [
|
const coreFeatures = [
|
||||||
{
|
{
|
||||||
id: 'news-catalyst',
|
id: 'news-catalyst',
|
||||||
title: '新闻中心',
|
title: '新闻催化分析',
|
||||||
description: '实时新闻事件分析,捕捉市场催化因子',
|
description: '实时新闻事件分析,捕捉市场催化因子',
|
||||||
icon: '📊',
|
icon: '📊',
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
url: '/community',
|
url: 'https://valuefrontier.cn/community',
|
||||||
badge: '核心',
|
badge: '核心',
|
||||||
featured: true
|
featured: true
|
||||||
},
|
},
|
||||||
@@ -64,7 +65,7 @@ export default function HomePage() {
|
|||||||
description: '热门概念与主题投资分析追踪',
|
description: '热门概念与主题投资分析追踪',
|
||||||
icon: '🎯',
|
icon: '🎯',
|
||||||
color: 'purple',
|
color: 'purple',
|
||||||
url: '/concepts',
|
url: 'https://valuefrontier.cn/concepts',
|
||||||
badge: '热门'
|
badge: '热门'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -73,7 +74,7 @@ export default function HomePage() {
|
|||||||
description: '全面的个股基本面信息整合',
|
description: '全面的个股基本面信息整合',
|
||||||
icon: '📈',
|
icon: '📈',
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
url: '/stocks',
|
url: 'https://valuefrontier.cn/stocks',
|
||||||
badge: '全面'
|
badge: '全面'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -82,7 +83,7 @@ export default function HomePage() {
|
|||||||
description: '涨停板数据深度分析与规律挖掘',
|
description: '涨停板数据深度分析与规律挖掘',
|
||||||
icon: '🚀',
|
icon: '🚀',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
url: '/limit-analyse',
|
url: 'https://valuefrontier.cn/limit-analyse',
|
||||||
badge: '精准'
|
badge: '精准'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -91,7 +92,7 @@ export default function HomePage() {
|
|||||||
description: '个股全方位分析与投资决策支持',
|
description: '个股全方位分析与投资决策支持',
|
||||||
icon: '🧭',
|
icon: '🧭',
|
||||||
color: 'orange',
|
color: 'orange',
|
||||||
url: '/company?scode=688256',
|
url: 'https://valuefrontier.cn/company?scode=688256',
|
||||||
badge: '专业'
|
badge: '专业'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -105,6 +106,15 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 个人中心配置
|
||||||
|
// const personalCenter = {
|
||||||
|
// title: '个人中心',
|
||||||
|
// description: '账户管理与个人设置',
|
||||||
|
// icon: '👤',
|
||||||
|
// color: 'gray',
|
||||||
|
// url: 'https://valuefrontier.cn/home/center'
|
||||||
|
// };
|
||||||
|
|
||||||
// @TODO 如何区分内部链接和外部链接?
|
// @TODO 如何区分内部链接和外部链接?
|
||||||
const handleProductClick = (url) => {
|
const handleProductClick = (url) => {
|
||||||
if (url.startsWith('http')) {
|
if (url.startsWith('http')) {
|
||||||
@@ -192,7 +202,7 @@ export default function HomePage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Container maxW="7xl" position="relative" zIndex={30} px={containerPx}>
|
<Container maxW="7xl" position="relative" zIndex={2} px={containerPx}>
|
||||||
<VStack spacing={{ base: 8, md: 12, lg: 16 }} align="stretch" minH={heroHeight} justify="center">
|
<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 }}>
|
<VStack spacing={{ base: 4, md: 5, lg: 6 }} textAlign="center" pt={{ base: 4, md: 6, lg: 8 }}>
|
||||||
@@ -215,7 +225,7 @@ export default function HomePage() {
|
|||||||
<Box pb={{ base: 8, md: 12 }}>
|
<Box pb={{ base: 8, md: 12 }}>
|
||||||
<VStack spacing={{ base: 6, md: 8 }}>
|
<VStack spacing={{ base: 6, md: 8 }}>
|
||||||
|
|
||||||
{/* 新闻中心 - 突出显示 */}
|
{/* 新闻催化分析 - 突出显示 */}
|
||||||
<Card
|
<Card
|
||||||
bg="transparent"
|
bg="transparent"
|
||||||
border="2px solid"
|
border="2px solid"
|
||||||
@@ -237,49 +247,79 @@ export default function HomePage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardBody p={{ base: 6, md: 8 }} position="relative" zIndex={1}>
|
<CardBody p={{ base: 6, md: 8 }} position="relative" zIndex={1}>
|
||||||
{/* 响应式布局:移动端纵向,桌面端横向 */}
|
{isMobile ? (
|
||||||
<Flex
|
/* 移动端:垂直布局 */
|
||||||
direction={{ base: 'column', md: 'row' }}
|
<VStack spacing={4} align="stretch">
|
||||||
align={{ base: 'stretch', md: 'center' }}
|
<HStack spacing={4}>
|
||||||
justify={{ base: 'flex-start', md: 'space-between' }}
|
|
||||||
gap={{ base: 4, md: 6 }}
|
|
||||||
>
|
|
||||||
<Flex align="center" gap={{ base: 4, md: 6 }} flex={1}>
|
|
||||||
<Box
|
<Box
|
||||||
p={{ base: 3, md: 4 }}
|
p={3}
|
||||||
borderRadius={{ base: 'lg', md: 'xl' }}
|
borderRadius="lg"
|
||||||
bg="yellow.400"
|
bg="yellow.400"
|
||||||
color="black"
|
color="black"
|
||||||
>
|
>
|
||||||
<Text fontSize={{ base: '2xl', md: '3xl' }}>{coreFeatures[0].icon}</Text>
|
<Text fontSize="2xl">{coreFeatures[0].icon}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<VStack align="start" spacing={{ base: 1, md: 2 }} flex={1}>
|
<VStack align="start" spacing={1} flex={1}>
|
||||||
<HStack>
|
<Heading size="lg" color="white">
|
||||||
<Heading size={{ base: 'lg', md: 'xl' }} color="white">
|
|
||||||
{coreFeatures[0].title}
|
{coreFeatures[0].title}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Badge colorScheme="yellow" variant="solid" fontSize={{ base: 'xs', md: 'sm' }}>
|
<Badge colorScheme="yellow" variant="solid" fontSize="xs">
|
||||||
|
{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}
|
{coreFeatures[0].badge}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text color="whiteAlpha.800" fontSize={{ base: 'md', md: 'lg' }} maxW={{ md: 'md' }} lineHeight="tall">
|
<Text color="whiteAlpha.800" fontSize="lg" maxW="md">
|
||||||
{coreFeatures[0].description}
|
{coreFeatures[0].description}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Flex>
|
</HStack>
|
||||||
<Button
|
<Button
|
||||||
colorScheme="yellow"
|
colorScheme="yellow"
|
||||||
size={{ base: 'md', md: 'lg' }}
|
size="lg"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
w={{ base: '100%', md: 'auto' }}
|
|
||||||
onClick={() => handleProductClick(coreFeatures[0].url)}
|
onClick={() => handleProductClick(coreFeatures[0].url)}
|
||||||
minH="44px"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
>
|
||||||
进入功能 →
|
进入功能 →
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -293,6 +333,7 @@ export default function HomePage() {
|
|||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor="whiteAlpha.200"
|
borderColor="whiteAlpha.200"
|
||||||
borderRadius={{ base: 'xl', md: '2xl' }}
|
borderRadius={{ base: 'xl', md: '2xl' }}
|
||||||
|
cursor="pointer"
|
||||||
transition="all 0.3s ease"
|
transition="all 0.3s ease"
|
||||||
_hover={{
|
_hover={{
|
||||||
bg: 'whiteAlpha.200',
|
bg: 'whiteAlpha.200',
|
||||||
|
|||||||
Reference in New Issue
Block a user