更新app.py
This commit is contained in:
Binary file not shown.
105
mcp_server.py
105
mcp_server.py
@@ -2868,7 +2868,8 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
if not successful_results and step_index == 0:
|
if not successful_results and step_index == 0:
|
||||||
# 如果没有执行任何工具(模型直接回复),使用模型的回复
|
# 如果没有执行任何工具(模型直接回复),使用模型的回复
|
||||||
if assistant_message and assistant_message.content:
|
if assistant_message and assistant_message.content:
|
||||||
final_summary = assistant_message.content
|
# 清理模型输出中的特殊标记(<think>、minimax:tool_call 等)
|
||||||
|
final_summary = clean_deepseek_tool_markers(assistant_message.content)
|
||||||
# 流式发送(虽然已经是完整的,但保持前端兼容)
|
# 流式发送(虽然已经是完整的,但保持前端兼容)
|
||||||
yield self._format_sse("summary_chunk", {"content": final_summary})
|
yield self._format_sse("summary_chunk", {"content": final_summary})
|
||||||
else:
|
else:
|
||||||
@@ -2890,16 +2891,58 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
stream=True, # 启用流式输出
|
stream=True, # 启用流式输出
|
||||||
)
|
)
|
||||||
|
|
||||||
# 逐块发送总结内容
|
# 逐块发送总结内容(带缓冲过滤特殊标签)
|
||||||
|
buffer = "" # 缓冲区,用于检测和过滤 <think> 等标签
|
||||||
|
in_think_tag = False # 是否在 <think> 标签内
|
||||||
|
in_minimax_tag = False # 是否在 minimax:tool_call 标签内
|
||||||
|
|
||||||
for chunk in summary_stream:
|
for chunk in summary_stream:
|
||||||
if chunk.choices and chunk.choices[0].delta.content:
|
if chunk.choices and chunk.choices[0].delta.content:
|
||||||
content_chunk = chunk.choices[0].delta.content
|
content_chunk = chunk.choices[0].delta.content
|
||||||
final_summary += content_chunk
|
final_summary += content_chunk
|
||||||
|
buffer += content_chunk
|
||||||
|
|
||||||
# 发送总结片段
|
# 检测 <think> 开始
|
||||||
yield self._format_sse("summary_chunk", {
|
if '<think>' in buffer and not in_think_tag:
|
||||||
"content": content_chunk
|
in_think_tag = True
|
||||||
})
|
# 发送 <think> 之前的内容
|
||||||
|
before_think = buffer.split('<think>')[0]
|
||||||
|
if before_think:
|
||||||
|
yield self._format_sse("summary_chunk", {"content": before_think})
|
||||||
|
buffer = '<think>' + buffer.split('<think>', 1)[1]
|
||||||
|
|
||||||
|
# 检测 </think> 结束
|
||||||
|
if '</think>' in buffer and in_think_tag:
|
||||||
|
in_think_tag = False
|
||||||
|
# 丢弃 <think>...</think> 内容,保留之后的内容
|
||||||
|
buffer = buffer.split('</think>', 1)[1].lstrip()
|
||||||
|
|
||||||
|
# 检测 minimax:tool_call 开始
|
||||||
|
if 'minimax:tool_call' in buffer and not in_minimax_tag:
|
||||||
|
in_minimax_tag = True
|
||||||
|
before_minimax = buffer.split('minimax:tool_call')[0]
|
||||||
|
if before_minimax:
|
||||||
|
yield self._format_sse("summary_chunk", {"content": before_minimax})
|
||||||
|
buffer = 'minimax:tool_call' + buffer.split('minimax:tool_call', 1)[1]
|
||||||
|
|
||||||
|
# 检测 </minimax:tool_call> 结束
|
||||||
|
if '</minimax:tool_call>' in buffer and in_minimax_tag:
|
||||||
|
in_minimax_tag = False
|
||||||
|
buffer = buffer.split('</minimax:tool_call>', 1)[1].lstrip()
|
||||||
|
|
||||||
|
# 如果不在特殊标签内,且缓冲区有足够内容,发送出去
|
||||||
|
if not in_think_tag and not in_minimax_tag:
|
||||||
|
# 保留一些缓冲以防标签跨 chunk
|
||||||
|
if len(buffer) > 50:
|
||||||
|
to_send = buffer[:-50]
|
||||||
|
buffer = buffer[-50:]
|
||||||
|
yield self._format_sse("summary_chunk", {"content": to_send})
|
||||||
|
|
||||||
|
# 发送剩余缓冲区内容(最终清理)
|
||||||
|
if buffer and not in_think_tag and not in_minimax_tag:
|
||||||
|
buffer = clean_deepseek_tool_markers(buffer)
|
||||||
|
if buffer:
|
||||||
|
yield self._format_sse("summary_chunk", {"content": buffer})
|
||||||
|
|
||||||
logger.info("[Summary] 流式总结完成")
|
logger.info("[Summary] 流式总结完成")
|
||||||
|
|
||||||
@@ -2914,7 +2957,8 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
yield self._format_sse("summary_chunk", {"content": final_summary})
|
yield self._format_sse("summary_chunk", {"content": final_summary})
|
||||||
logger.warning("[Summary] 使用降级方案")
|
logger.warning("[Summary] 使用降级方案")
|
||||||
|
|
||||||
# 发送完整的总结和元数据
|
# 发送完整的总结和元数据(最终清理确保无残留标记)
|
||||||
|
final_summary = clean_deepseek_tool_markers(final_summary)
|
||||||
yield self._format_sse("summary", {
|
yield self._format_sse("summary", {
|
||||||
"content": final_summary,
|
"content": final_summary,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@@ -3090,6 +3134,30 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
|||||||
"arguments": arguments
|
"arguments": arguments
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 格式5: MiniMax 格式
|
||||||
|
# minimax:tool_call <invoke name="get_market_overview"> <parameter name="date">20260129</parameter> </invoke> </minimax:tool_call>
|
||||||
|
minimax_pattern = r'minimax:tool_call\s*<invoke\s+name="(\w+)">(.*?)</invoke>\s*</minimax:tool_call>'
|
||||||
|
minimax_matches = re.findall(minimax_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
for func_name, params_str in minimax_matches:
|
||||||
|
arguments = {}
|
||||||
|
# 解析参数: <parameter name="xxx">value</parameter>
|
||||||
|
param_pattern = r'<parameter\s+name="(\w+)">(.*?)</parameter>'
|
||||||
|
param_matches = re.findall(param_pattern, params_str, re.DOTALL)
|
||||||
|
|
||||||
|
for param_name, param_value in param_matches:
|
||||||
|
param_value = param_value.strip()
|
||||||
|
# 尝试解析 JSON 值,否则作为字符串
|
||||||
|
try:
|
||||||
|
arguments[param_name] = json.loads(param_value)
|
||||||
|
except:
|
||||||
|
arguments[param_name] = param_value
|
||||||
|
|
||||||
|
tool_calls.append({
|
||||||
|
"name": func_name,
|
||||||
|
"arguments": arguments
|
||||||
|
})
|
||||||
|
|
||||||
logger.info(f"[Text Tool Call] 解析到 {len(tool_calls)} 个工具调用: {tool_calls}")
|
logger.info(f"[Text Tool Call] 解析到 {len(tool_calls)} 个工具调用: {tool_calls}")
|
||||||
return tool_calls
|
return tool_calls
|
||||||
|
|
||||||
@@ -3504,18 +3572,33 @@ MEETING_MODEL_CONFIGS = {
|
|||||||
|
|
||||||
def clean_deepseek_tool_markers(content: str) -> str:
|
def clean_deepseek_tool_markers(content: str) -> str:
|
||||||
"""
|
"""
|
||||||
清理 DeepSeek 模型输出中的工具调用标记
|
清理模型输出中的工具调用标记和思考标签
|
||||||
DeepSeek 有时会以文本形式输出工具调用,格式如:
|
支持多种模型格式:
|
||||||
<|tool▁calls▁begin|><|tool▁call▁begin|>tool_name<|tool▁sep|>{"args": "value"}<|tool▁call▁end|><|tool▁calls▁end|>
|
1. DeepSeek: <|tool▁calls▁begin|>...<|tool▁calls▁end|>
|
||||||
|
2. MiniMax: minimax:tool_call <invoke>...</invoke> </minimax:tool_call>
|
||||||
|
3. 思考标签: <think>...</think>
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
if not content:
|
if not content:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
# 清理 <think>...</think> 思考标签(多种模型都可能输出)
|
||||||
|
cleaned = re.sub(r'<think>.*?</think>\s*', '', content, flags=re.DOTALL)
|
||||||
|
|
||||||
|
# 清理 MiniMax 工具调用标记
|
||||||
|
# 格式: minimax:tool_call <invoke name="xxx">...</invoke> </minimax:tool_call>
|
||||||
|
cleaned = re.sub(r'minimax:tool_call\s*<invoke[^>]*>.*?</invoke>\s*</minimax:tool_call>\s*', '', cleaned, flags=re.DOTALL)
|
||||||
|
|
||||||
# 清理 DeepSeek 工具调用标记
|
# 清理 DeepSeek 工具调用标记
|
||||||
# 匹配 <|tool▁calls▁begin|> ... <|tool▁calls▁end|> 整个块
|
# 匹配 <|tool▁calls▁begin|> ... <|tool▁calls▁end|> 整个块
|
||||||
pattern = r'<|tool▁calls▁begin|>.*?<|tool▁calls▁end|>'
|
pattern = r'<|tool▁calls▁begin|>.*?<|tool▁calls▁end|>'
|
||||||
cleaned = re.sub(pattern, '', content, flags=re.DOTALL)
|
cleaned = re.sub(pattern, '', cleaned, flags=re.DOTALL)
|
||||||
|
|
||||||
|
# 清理 DSML 格式工具调用
|
||||||
|
cleaned = re.sub(r'<[|\|]DSML[|\|]function_calls>.*?</[|\|]DSML[|\|]function_calls>\s*', '', cleaned, flags=re.DOTALL)
|
||||||
|
|
||||||
|
# 清理通用 <tool_call>...</tool_call> 格式
|
||||||
|
cleaned = re.sub(r'<tool_call>.*?</tool_call>\s*', '', cleaned, flags=re.DOTALL)
|
||||||
|
|
||||||
# 也清理可能残留的单个标记
|
# 也清理可能残留的单个标记
|
||||||
markers = [
|
markers = [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Form, Input, Button, Typography, Modal, message, Tooltip } from 'antd';
|
import { Form, Input, Button, Typography, Modal, message, Tooltip } from 'antd';
|
||||||
import { LockOutlined, WechatOutlined, MobileOutlined, QrcodeOutlined } from '@ant-design/icons';
|
import { LockOutlined, WechatOutlined, MobileOutlined, QrcodeOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
import { useBreakpointValue } from "@chakra-ui/react";
|
import { useBreakpointValue } from "@chakra-ui/react";
|
||||||
import { useAuth } from "../../contexts/AuthContext";
|
import { useAuth } from "../../contexts/AuthContext";
|
||||||
import { useAuthModal } from "../../hooks/useAuthModal";
|
import { useAuthModal } from "../../hooks/useAuthModal";
|
||||||
@@ -162,6 +162,11 @@ const AUTH_CONFIG = {
|
|||||||
title: "手机号登录",
|
title: "手机号登录",
|
||||||
subtitle: "未注册手机号登录时将自动创建价值前沿账号",
|
subtitle: "未注册手机号登录时将自动创建价值前沿账号",
|
||||||
},
|
},
|
||||||
|
// 密码登录内容区文案
|
||||||
|
password: {
|
||||||
|
title: "账号密码登录",
|
||||||
|
subtitle: "使用用户名、手机号或邮箱登录",
|
||||||
|
},
|
||||||
buttonText: "登录/注册",
|
buttonText: "登录/注册",
|
||||||
loadingText: "验证中...",
|
loadingText: "验证中...",
|
||||||
successDescription: "欢迎!",
|
successDescription: "欢迎!",
|
||||||
@@ -192,6 +197,7 @@ export default function AuthFormContent() {
|
|||||||
const [showNicknamePrompt, setShowNicknamePrompt] = useState(false);
|
const [showNicknamePrompt, setShowNicknamePrompt] = useState(false);
|
||||||
const [currentPhone, setCurrentPhone] = useState("");
|
const [currentPhone, setCurrentPhone] = useState("");
|
||||||
const [formData, setFormData] = useState({ phone: "", verificationCode: "" });
|
const [formData, setFormData] = useState({ phone: "", verificationCode: "" });
|
||||||
|
const [passwordFormData, setPasswordFormData] = useState({ username: "", password: "" });
|
||||||
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
|
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
|
||||||
const [sendingCode, setSendingCode] = useState(false);
|
const [sendingCode, setSendingCode] = useState(false);
|
||||||
const [countdown, setCountdown] = useState(0);
|
const [countdown, setCountdown] = useState(0);
|
||||||
@@ -365,6 +371,52 @@ export default function AuthFormContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 密码登录输入变化处理
|
||||||
|
const handlePasswordInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setPasswordFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 密码登录提交
|
||||||
|
const handlePasswordSubmit = async (e) => {
|
||||||
|
e?.preventDefault?.();
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { username, password } = passwordFormData;
|
||||||
|
if (!username || !password) {
|
||||||
|
message.warning("用户名和密码不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
authEvents.trackLoginPageViewed(); // 追踪密码登录尝试
|
||||||
|
|
||||||
|
const data = await authService.loginWithPassword(username, password);
|
||||||
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
await checkSession();
|
||||||
|
authEvents.trackLoginSuccess(data.user, 'password', false);
|
||||||
|
message.success('登录成功');
|
||||||
|
setTimeout(() => handleLoginSuccess({ username }), config.features.successDelay);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (showWelcomeGuide) showWelcomeGuide();
|
||||||
|
}, 10000);
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || '登录失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
authEvents.trackLoginFailed('password', 'api', error.message, {
|
||||||
|
username_masked: passwordFormData.username ? passwordFormData.username.substring(0, 2) + '****' : 'N/A',
|
||||||
|
});
|
||||||
|
logger.error('AuthFormContent', 'handlePasswordSubmit', error);
|
||||||
|
message.error(error.message || "登录失败");
|
||||||
|
} finally {
|
||||||
|
if (isMountedRef.current) setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 微信H5登录(移动端)
|
// 微信H5登录(移动端)
|
||||||
const handleWechatH5Login = async () => {
|
const handleWechatH5Login = async () => {
|
||||||
authEvents.trackWechatLoginInitiated('icon_button');
|
authEvents.trackWechatLoginInitiated('icon_button');
|
||||||
@@ -477,6 +529,78 @@ export default function AuthFormContent() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 渲染密码登录表单
|
||||||
|
const renderPasswordForm = () => (
|
||||||
|
<div>
|
||||||
|
{/* 内容区小标题 */}
|
||||||
|
<div style={styles.contentTitle}>
|
||||||
|
<span style={styles.contentTitleText}>{config.password.title}</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.contentSubtitle}>{config.password.subtitle}</div>
|
||||||
|
|
||||||
|
<Form layout="vertical" onFinish={handlePasswordSubmit}>
|
||||||
|
<Form.Item style={{ marginBottom: '16px' }}>
|
||||||
|
<Input
|
||||||
|
name="username"
|
||||||
|
value={passwordFormData.username}
|
||||||
|
onChange={handlePasswordInputChange}
|
||||||
|
placeholder="用户名 / 手机号 / 邮箱"
|
||||||
|
size="large"
|
||||||
|
style={styles.input}
|
||||||
|
prefix={<UserOutlined style={{ color: THEME.textMuted }} />}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: '24px' }}>
|
||||||
|
<Input.Password
|
||||||
|
name="password"
|
||||||
|
value={passwordFormData.password}
|
||||||
|
onChange={handlePasswordInputChange}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
size="large"
|
||||||
|
style={styles.input}
|
||||||
|
prefix={<LockOutlined style={{ color: THEME.textMuted }} />}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: '48px', marginTop: '16px' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
loading={isLoading}
|
||||||
|
icon={<LockOutlined />}
|
||||||
|
style={styles.submitBtn}
|
||||||
|
>
|
||||||
|
{isLoading ? config.loadingText : "登录"}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={styles.privacyText}>
|
||||||
|
登录即表示您同意价值前沿{" "}
|
||||||
|
<Link
|
||||||
|
href="/home/user-agreement"
|
||||||
|
target="_blank"
|
||||||
|
onClick={authEvents.trackUserAgreementClicked}
|
||||||
|
style={styles.privacyLink}
|
||||||
|
>
|
||||||
|
《用户协议》
|
||||||
|
</Link>
|
||||||
|
{" "}和{" "}
|
||||||
|
<Link
|
||||||
|
href="/home/privacy-policy"
|
||||||
|
target="_blank"
|
||||||
|
onClick={authEvents.trackPrivacyPolicyClicked}
|
||||||
|
style={styles.privacyLink}
|
||||||
|
>
|
||||||
|
《隐私政策》
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
// 渲染微信登录区域
|
// 渲染微信登录区域
|
||||||
const renderWechatLogin = () => (
|
const renderWechatLogin = () => (
|
||||||
<div>
|
<div>
|
||||||
@@ -506,79 +630,117 @@ export default function AuthFormContent() {
|
|||||||
|
|
||||||
// 渲染底部其他登录方式
|
// 渲染底部其他登录方式
|
||||||
const renderBottomDivider = () => {
|
const renderBottomDivider = () => {
|
||||||
const isWechatTab = activeTab === 'wechat';
|
|
||||||
|
|
||||||
// 移动端微信 Tab 下不显示底部切换(因为移动端微信是 H5 跳转)
|
// 移动端微信 Tab 下不显示底部切换(因为移动端微信是 H5 跳转)
|
||||||
if (isMobile && isWechatTab) {
|
if (isMobile && activeTab === 'wechat') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据当前 tab 显示其他两种登录方式
|
||||||
|
const otherOptions = [];
|
||||||
|
|
||||||
|
if (activeTab !== 'wechat') {
|
||||||
|
otherOptions.push({
|
||||||
|
key: 'wechat',
|
||||||
|
icon: <WechatOutlined />,
|
||||||
|
label: '微信',
|
||||||
|
color: THEME.wechat,
|
||||||
|
hoverBg: 'rgba(7, 193, 96, 0.1)',
|
||||||
|
onClick: isMobile ? handleWechatH5Login : () => handleTabChange('wechat'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab !== 'phone') {
|
||||||
|
otherOptions.push({
|
||||||
|
key: 'phone',
|
||||||
|
icon: <MobileOutlined />,
|
||||||
|
label: '验证码',
|
||||||
|
color: THEME.goldPrimary,
|
||||||
|
hoverBg: 'rgba(212, 175, 55, 0.1)',
|
||||||
|
onClick: () => handleTabChange('phone'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTab !== 'password') {
|
||||||
|
otherOptions.push({
|
||||||
|
key: 'password',
|
||||||
|
icon: <LockOutlined />,
|
||||||
|
label: '密码',
|
||||||
|
color: THEME.textSecondary,
|
||||||
|
hoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
onClick: () => handleTabChange('password'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.bottomDivider}>
|
<div style={styles.bottomDivider}>
|
||||||
<div style={styles.dividerLine} />
|
<div style={styles.dividerLine} />
|
||||||
<div style={styles.otherLoginWrapper}>
|
<div style={styles.otherLoginWrapper}>
|
||||||
<span>其他登录方式:</span>
|
<span>其他登录方式:</span>
|
||||||
{isWechatTab ? (
|
{otherOptions.map((option) => (
|
||||||
<span
|
<span
|
||||||
|
key={option.key}
|
||||||
style={{
|
style={{
|
||||||
...styles.otherLoginIcon,
|
...styles.otherLoginIcon,
|
||||||
color: THEME.goldPrimary,
|
color: option.color,
|
||||||
}}
|
}}
|
||||||
onClick={() => handleTabChange('phone')}
|
onClick={option.onClick}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = 'rgba(212, 175, 55, 0.1)';
|
e.currentTarget.style.background = option.hoverBg;
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = 'transparent';
|
e.currentTarget.style.background = 'transparent';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MobileOutlined /> 手机
|
{option.icon} {option.label}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
))}
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
...styles.otherLoginIcon,
|
|
||||||
color: THEME.wechat,
|
|
||||||
}}
|
|
||||||
onClick={isMobile ? handleWechatH5Login : () => handleTabChange('wechat')}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.background = 'rgba(7, 193, 96, 0.1)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.background = 'transparent';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WechatOutlined /> 微信
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.dividerLine} />
|
<div style={styles.dividerLine} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取下一个登录方式(循环切换:wechat -> phone -> password -> wechat)
|
||||||
|
const getNextTab = () => {
|
||||||
|
if (activeTab === 'wechat') return 'phone';
|
||||||
|
if (activeTab === 'phone') return 'password';
|
||||||
|
return 'wechat'; // password -> wechat
|
||||||
|
};
|
||||||
|
|
||||||
// 获取右上角折角的颜色(根据要切换到的登录方式)
|
// 获取右上角折角的颜色(根据要切换到的登录方式)
|
||||||
const getCornerColor = () => {
|
const getCornerColor = () => {
|
||||||
// 显示要切换到的方式的颜色
|
const nextTab = getNextTab();
|
||||||
return activeTab === 'wechat' ? THEME.goldPrimary : THEME.wechat;
|
if (nextTab === 'wechat') return THEME.wechat;
|
||||||
|
if (nextTab === 'phone') return THEME.goldPrimary;
|
||||||
|
return THEME.textSecondary; // password
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取右上角图标
|
// 获取右上角图标
|
||||||
const getCornerIcon = () => {
|
const getCornerIcon = () => {
|
||||||
// 显示要切换到的方式的图标:手机图标 或 二维码图标
|
const nextTab = getNextTab();
|
||||||
return activeTab === 'wechat' ? <MobileOutlined /> : <QrcodeOutlined />;
|
if (nextTab === 'wechat') return <QrcodeOutlined />;
|
||||||
|
if (nextTab === 'phone') return <MobileOutlined />;
|
||||||
|
return <LockOutlined />; // password
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取右上角提示文字
|
||||||
|
const getCornerTooltip = () => {
|
||||||
|
const nextTab = getNextTab();
|
||||||
|
if (nextTab === 'wechat') return '切换到微信登录';
|
||||||
|
if (nextTab === 'phone') return '切换到验证码登录';
|
||||||
|
return '切换到密码登录';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="auth-form-content" style={{ position: 'relative' }}>
|
<div className="auth-form-content" style={{ position: 'relative' }}>
|
||||||
{/* 右上角折角切换图标 */}
|
{/* 右上角折角切换图标 */}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={activeTab === 'wechat' ? '切换到验证码登录' : '切换到微信登录'}
|
title={getCornerTooltip()}
|
||||||
placement="left"
|
placement="left"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={styles.cornerSwitch}
|
style={styles.cornerSwitch}
|
||||||
onClick={() => handleTabChange(activeTab === 'wechat' ? 'phone' : 'wechat')}
|
onClick={() => handleTabChange(getNextTab())}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -597,7 +759,9 @@ export default function AuthFormContent() {
|
|||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<div style={styles.contentArea}>
|
<div style={styles.contentArea}>
|
||||||
{activeTab === 'wechat' ? renderWechatLogin() : renderPhoneForm()}
|
{activeTab === 'wechat' && renderWechatLogin()}
|
||||||
|
{activeTab === 'phone' && renderPhoneForm()}
|
||||||
|
{activeTab === 'password' && renderPasswordForm()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部其他登录方式 */}
|
{/* 底部其他登录方式 */}
|
||||||
|
|||||||
@@ -134,6 +134,50 @@ export const authService = {
|
|||||||
body: JSON.stringify({ session_id: sessionId }),
|
body: JSON.stringify({ session_id: sessionId }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用用户名/手机号/邮箱 + 密码登录
|
||||||
|
* @param {string} username - 用户名、手机号或邮箱
|
||||||
|
* @param {string} password - 密码
|
||||||
|
* @returns {Promise<{success: boolean, user?: object, error?: string}>}
|
||||||
|
*/
|
||||||
|
loginWithPassword: async (username, password) => {
|
||||||
|
const method = 'POST';
|
||||||
|
const url = '/api/auth/login';
|
||||||
|
|
||||||
|
logger.api.request(method, url, { username: username });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 后端使用 form-data 格式
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
formData.append('username', username);
|
||||||
|
formData.append('password', password);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
logger.api.response(method, url, response.status, data);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || data.message || `登录失败 (${response.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
logger.api.error(method, url, error, { username });
|
||||||
|
if (error.message === 'Failed to fetch' || error.name === 'TypeError') {
|
||||||
|
throw new Error('网络连接失败,请检查网络设置');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user