更新app.py

This commit is contained in:
2026-01-31 15:32:00 +08:00
parent 1f2af549f5
commit c56f5f2f7f
4 changed files with 335 additions and 44 deletions

View File

@@ -3,7 +3,7 @@
import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
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 { useAuth } from "../../contexts/AuthContext";
import { useAuthModal } from "../../hooks/useAuthModal";
@@ -162,6 +162,11 @@ const AUTH_CONFIG = {
title: "手机号登录",
subtitle: "未注册手机号登录时将自动创建价值前沿账号",
},
// 密码登录内容区文案
password: {
title: "账号密码登录",
subtitle: "使用用户名、手机号或邮箱登录",
},
buttonText: "登录/注册",
loadingText: "验证中...",
successDescription: "欢迎!",
@@ -192,6 +197,7 @@ export default function AuthFormContent() {
const [showNicknamePrompt, setShowNicknamePrompt] = useState(false);
const [currentPhone, setCurrentPhone] = useState("");
const [formData, setFormData] = useState({ phone: "", verificationCode: "" });
const [passwordFormData, setPasswordFormData] = useState({ username: "", password: "" });
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
const [sendingCode, setSendingCode] = useState(false);
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登录移动端
const handleWechatH5Login = async () => {
authEvents.trackWechatLoginInitiated('icon_button');
@@ -477,6 +529,78 @@ export default function AuthFormContent() {
</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 = () => (
<div>
@@ -506,79 +630,117 @@ export default function AuthFormContent() {
// 渲染底部其他登录方式
const renderBottomDivider = () => {
const isWechatTab = activeTab === 'wechat';
// 移动端微信 Tab 下不显示底部切换(因为移动端微信是 H5 跳转)
if (isMobile && isWechatTab) {
if (isMobile && activeTab === 'wechat') {
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 (
<div style={styles.bottomDivider}>
<div style={styles.dividerLine} />
<div style={styles.otherLoginWrapper}>
<span>其他登录方式:</span>
{isWechatTab ? (
{otherOptions.map((option) => (
<span
key={option.key}
style={{
...styles.otherLoginIcon,
color: THEME.goldPrimary,
color: option.color,
}}
onClick={() => handleTabChange('phone')}
onClick={option.onClick}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(212, 175, 55, 0.1)';
e.currentTarget.style.background = option.hoverBg;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
<MobileOutlined /> 手机
{option.icon} {option.label}
</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 style={styles.dividerLine} />
</div>
);
};
// 获取下一个登录方式循环切换wechat -> phone -> password -> wechat
const getNextTab = () => {
if (activeTab === 'wechat') return 'phone';
if (activeTab === 'phone') return 'password';
return 'wechat'; // password -> wechat
};
// 获取右上角折角的颜色(根据要切换到的登录方式)
const getCornerColor = () => {
// 显示要切换到的方式的颜色
return activeTab === 'wechat' ? THEME.goldPrimary : THEME.wechat;
const nextTab = getNextTab();
if (nextTab === 'wechat') return THEME.wechat;
if (nextTab === 'phone') return THEME.goldPrimary;
return THEME.textSecondary; // password
};
// 获取右上角图标
const getCornerIcon = () => {
// 显示要切换到的方式的图标:手机图标 或 二维码图标
return activeTab === 'wechat' ? <MobileOutlined /> : <QrcodeOutlined />;
const nextTab = getNextTab();
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 (
<div className="auth-form-content" style={{ position: 'relative' }}>
{/* 右上角折角切换图标 */}
<Tooltip
title={activeTab === 'wechat' ? '切换到验证码登录' : '切换到微信登录'}
title={getCornerTooltip()}
placement="left"
>
<div
style={styles.cornerSwitch}
onClick={() => handleTabChange(activeTab === 'wechat' ? 'phone' : 'wechat')}
onClick={() => handleTabChange(getNextTab())}
>
<div
style={{
@@ -597,7 +759,9 @@ export default function AuthFormContent() {
{/* 内容区域 */}
<div style={styles.contentArea}>
{activeTab === 'wechat' ? renderWechatLogin() : renderPhoneForm()}
{activeTab === 'wechat' && renderWechatLogin()}
{activeTab === 'phone' && renderPhoneForm()}
{activeTab === 'password' && renderPasswordForm()}
</div>
{/* 底部其他登录方式 */}

View File

@@ -134,6 +134,50 @@ export const authService = {
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;
}
},
};
/**