style(Auth): 登录弹窗改为 Tab 切换布局

- 改为单列布局 (450px),右上角折角图标切换微信/手机登录
- 微信登录: 显示二维码图标,切换到手机显示手机图标
- 验证码登录: 添加内容区标题和副标题
- 关闭按钮移除,点击蒙层关闭弹窗
- 验证码倒计时按钮颜色改为金色
- 调整表单区域间距:上方紧凑,下方留白

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-30 17:35:55 +08:00
parent 0d05b69601
commit 97c10bf2cc
5 changed files with 444 additions and 116 deletions

View File

@@ -1,9 +1,9 @@
// src/components/Auth/AuthFormContent.js // src/components/Auth/AuthFormContent.js
// 统一的认证表单组件 - Ant Design 版本 - 黑金主题 // 统一的认证表单组件 - Ant Design 版本 - 黑金主题 - Tab 切换布局
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, Space, Row, Col, Modal, message } from 'antd'; import { Form, Input, Button, Typography, Modal, message, Tooltip } from 'antd';
import { LockOutlined, WechatOutlined } from '@ant-design/icons'; import { LockOutlined, WechatOutlined, MobileOutlined, QrcodeOutlined } 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";
@@ -16,7 +16,7 @@ import { logger } from '../../utils/logger';
import { getApiBase } from '../../utils/apiConfig'; import { getApiBase } from '../../utils/apiConfig';
import { useAuthEvents } from '../../hooks/useAuthEvents'; import { useAuthEvents } from '../../hooks/useAuthEvents';
const { Text, Link, Title } = Typography; const { Link } = Typography;
// 黑金主题样式 // 黑金主题样式
const THEME = { const THEME = {
@@ -27,18 +27,47 @@ const THEME = {
bgElevated: '#1A1A2E', bgElevated: '#1A1A2E',
lineDefault: 'rgba(212, 175, 55, 0.3)', lineDefault: 'rgba(212, 175, 55, 0.3)',
lineEmphasis: 'rgba(212, 175, 55, 0.5)', lineEmphasis: 'rgba(212, 175, 55, 0.5)',
lineSubtle: 'rgba(212, 175, 55, 0.15)',
textPrimary: 'rgba(255, 255, 255, 0.95)', textPrimary: 'rgba(255, 255, 255, 0.95)',
textSecondary: 'rgba(255, 255, 255, 0.6)',
textMuted: 'rgba(255, 255, 255, 0.4)', textMuted: 'rgba(255, 255, 255, 0.4)',
wechat: '#07C160', wechat: '#07C160',
}; };
const styles = { const styles = {
formTitle: { // 右上角折角切换图标
color: THEME.goldPrimary, cornerSwitch: {
fontWeight: 600, position: 'absolute',
letterSpacing: '0.05em', top: '-32px',
marginBottom: '16px', right: '-32px',
width: '64px',
height: '64px',
cursor: 'pointer',
zIndex: 10,
overflow: 'hidden',
}, },
cornerTriangle: {
position: 'absolute',
top: 0,
right: 0,
width: 0,
height: 0,
borderStyle: 'solid',
borderWidth: '0 64px 64px 0',
transition: 'all 0.2s ease',
},
cornerIcon: {
position: 'absolute',
top: '12px',
right: '8px',
fontSize: '18px',
color: '#fff',
transform: 'rotate(0deg)',
},
// 内容区域
contentArea: {
},
// 输入框
input: { input: {
background: THEME.bgInput, background: THEME.bgInput,
border: `1px solid ${THEME.lineDefault}`, border: `1px solid ${THEME.lineDefault}`,
@@ -46,6 +75,7 @@ const styles = {
height: '44px', height: '44px',
borderRadius: '8px', borderRadius: '8px',
}, },
// 提交按钮
submitBtn: { submitBtn: {
height: '48px', height: '48px',
fontSize: '16px', fontSize: '16px',
@@ -56,31 +86,82 @@ const styles = {
color: THEME.bgElevated, color: THEME.bgElevated,
boxShadow: '0 4px 15px rgba(212, 175, 55, 0.3)', boxShadow: '0 4px 15px rgba(212, 175, 55, 0.3)',
}, },
// 隐私文本
privacyText: { privacyText: {
textAlign: 'center', textAlign: 'center',
fontSize: '12px', fontSize: '12px',
color: THEME.textMuted, color: THEME.textMuted,
marginTop: '12px', marginTop: '8px',
}, },
privacyLink: { privacyLink: {
color: THEME.goldPrimary, color: THEME.goldPrimary,
textDecoration: 'underline', textDecoration: 'underline',
fontSize: '12px',
}, },
wechatBtn: { // 底部分割线区域
color: THEME.wechat, bottomDivider: {
padding: '4px 8px', display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginTop: '24px',
gap: '12px',
}, },
otherLoginText: { dividerLine: {
flex: 1,
height: '1px',
background: `linear-gradient(to right, transparent, ${THEME.lineDefault}, transparent)`,
},
otherLoginWrapper: {
display: 'flex',
alignItems: 'center',
gap: '8px',
color: THEME.textMuted, color: THEME.textMuted,
fontSize: '12px', fontSize: '12px',
whiteSpace: 'nowrap',
},
otherLoginIcon: {
cursor: 'pointer',
padding: '4px 8px',
borderRadius: '4px',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
gap: '4px',
},
// 内容区小标题
contentTitle: {
textAlign: 'left',
marginBottom: '4px',
},
contentTitleText: {
color: THEME.goldPrimary,
fontSize: '20px',
fontWeight: 600,
letterSpacing: '0.05em',
},
contentSubtitle: {
textAlign: 'left',
color: THEME.textSecondary,
fontSize: '12px',
marginBottom: '24px',
}, },
}; };
// 统一配置对象 // 统一配置对象
const AUTH_CONFIG = { const AUTH_CONFIG = {
// 大标题(固定不变)
title: "价值前沿", title: "价值前沿",
subtitle: "开启您的投资之旅", subtitle: "开启您的投资之旅",
formTitle: "登陆/注册", // 微信登录内容区文案
wechat: {
title: "微信安全登录",
subtitle: "未注册的微信号登录时将自动创建价值前沿账号",
},
// 验证码登录内容区文案
phone: {
title: "手机号登录",
subtitle: "未注册手机号登录时将自动创建价值前沿账号",
},
buttonText: "登录/注册", buttonText: "登录/注册",
loadingText: "验证中...", loadingText: "验证中...",
successDescription: "欢迎!", successDescription: "欢迎!",
@@ -102,7 +183,10 @@ export default function AuthFormContent() {
const config = AUTH_CONFIG; const config = AUTH_CONFIG;
const isMountedRef = useRef(true); const isMountedRef = useRef(true);
// 状态 // Tab 状态: 'wechat' | 'phone'
const [activeTab, setActiveTab] = useState('wechat');
// 表单状态
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [showNicknamePrompt, setShowNicknamePrompt] = useState(false); const [showNicknamePrompt, setShowNicknamePrompt] = useState(false);
const [currentPhone, setCurrentPhone] = useState(""); const [currentPhone, setCurrentPhone] = useState("");
@@ -117,6 +201,12 @@ export default function AuthFormContent() {
// 事件追踪 // 事件追踪
const authEvents = useAuthEvents({ component: 'AuthFormContent', isMobile }); const authEvents = useAuthEvents({ component: 'AuthFormContent', isMobile });
// 切换 Tab
const handleTabChange = (tab) => {
setActiveTab(tab);
authEvents.trackLoginPageViewed(); // 追踪切换
};
// 输入变化处理 // 输入变化处理
const handleInputChange = (e) => { const handleInputChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
@@ -266,7 +356,7 @@ export default function AuthFormContent() {
} }
}; };
// 微信H5登录 // 微信H5登录(移动端)
const handleWechatH5Login = async () => { const handleWechatH5Login = async () => {
authEvents.trackWechatLoginInitiated('icon_button'); authEvents.trackWechatLoginInitiated('icon_button');
try { try {
@@ -295,96 +385,202 @@ export default function AuthFormContent() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// 渲染手机验证码登录表单
const renderPhoneForm = () => (
<div>
{/* 内容区小标题 */}
<div style={styles.contentTitle}>
<span style={styles.contentTitleText}>{config.phone.title}</span>
</div>
<div style={styles.contentSubtitle}>{config.phone.subtitle}</div>
<Form layout="vertical" onFinish={handleSubmit}>
<Form.Item style={{ marginBottom: '24px' }}>
<Input
name="phone"
value={formData.phone}
onChange={handleInputChange}
placeholder="请输入11位手机号"
size="large"
style={styles.input}
prefix={<MobileOutlined style={{ color: THEME.textMuted }} />}
/>
</Form.Item>
<Form.Item style={{ marginBottom: '24px' }}>
<VerificationCodeInput
value={formData.verificationCode}
onChange={handleInputChange}
onSendCode={sendVerificationCode}
countdown={countdown}
isLoading={isLoading}
isSending={sendingCode}
/>
</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 : config.buttonText}
</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>
<WechatRegister />
<div style={{ ...styles.privacyText, marginTop: '16px' }}>
扫码即表示同意{" "}
<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>
</div>
);
// 渲染底部其他登录方式
const renderBottomDivider = () => {
const isWechatTab = activeTab === 'wechat';
// 移动端微信 Tab 下不显示底部切换(因为移动端微信是 H5 跳转)
if (isMobile && isWechatTab) {
return null;
}
return (
<div style={styles.bottomDivider}>
<div style={styles.dividerLine} />
<div style={styles.otherLoginWrapper}>
<span>其他登录方式:</span>
{isWechatTab ? (
<span
style={{
...styles.otherLoginIcon,
color: THEME.goldPrimary,
}}
onClick={() => handleTabChange('phone')}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(212, 175, 55, 0.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
<MobileOutlined /> 手机
</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>
);
};
// 获取右上角折角的颜色(根据要切换到的登录方式)
const getCornerColor = () => {
// 显示要切换到的方式的颜色
return activeTab === 'wechat' ? THEME.goldPrimary : THEME.wechat;
};
// 获取右上角图标
const getCornerIcon = () => {
// 显示要切换到的方式的图标:手机图标 或 二维码图标
return activeTab === 'wechat' ? <MobileOutlined /> : <QrcodeOutlined />;
};
return ( return (
<div className="auth-form-content"> <div className="auth-form-content" style={{ position: 'relative' }}>
{/* 右上角折角切换图标 */}
<Tooltip
title={activeTab === 'wechat' ? '切换到验证码登录' : '切换到微信登录'}
placement="left"
>
<div
style={styles.cornerSwitch}
onClick={() => handleTabChange(activeTab === 'wechat' ? 'phone' : 'wechat')}
>
<div
style={{
...styles.cornerTriangle,
borderColor: `transparent ${getCornerColor()} transparent transparent`,
}}
/>
<span style={styles.cornerIcon}>
{getCornerIcon()}
</span>
</div>
</Tooltip>
{/* 大标题(固定不变) */}
<AuthHeader title={config.title} subtitle={config.subtitle} /> <AuthHeader title={config.title} subtitle={config.subtitle} />
<Row gutter={isMobile ? 0 : 32} align="top"> {/* 内容区域 */}
{/* 左侧表单 */} <div style={styles.contentArea}>
<Col xs={24} md={14}> {activeTab === 'wechat' ? renderWechatLogin() : renderPhoneForm()}
<Title level={5} style={styles.formTitle}>{config.formTitle}</Title> </div>
<Form layout="vertical" onFinish={handleSubmit}> {/* 底部其他登录方式 */}
<Form.Item> {renderBottomDivider()}
<Input
name="phone"
value={formData.phone}
onChange={handleInputChange}
placeholder="请输入11位手机号"
size="large"
style={styles.input}
/>
</Form.Item>
<Form.Item>
<VerificationCodeInput
value={formData.verificationCode}
onChange={handleInputChange}
onSendCode={sendVerificationCode}
countdown={countdown}
isLoading={isLoading}
isSending={sendingCode}
/>
</Form.Item>
{/* 移动端微信登录 */}
{isMobile && (
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '16px' }}>
<Text style={styles.otherLoginText}>其他登录方式</Text>
<Button
type="text"
icon={<WechatOutlined />}
onClick={handleWechatH5Login}
disabled={isLoading}
style={styles.wechatBtn}
/>
</div>
)}
<Form.Item>
<Button
type="primary"
htmlType="submit"
size="large"
block
loading={isLoading}
icon={<LockOutlined />}
style={styles.submitBtn}
>
{isLoading ? config.loadingText : config.buttonText}
</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>
</Col>
{/* 右侧微信二维码 - 仅桌面端 */}
{!isMobile && (
<Col xs={24} md={10}>
<WechatRegister />
</Col>
)}
</Row>
{/* 昵称设置引导 */} {/* 昵称设置引导 */}
<Modal <Modal

View File

@@ -58,10 +58,21 @@
.ant-modal-close { .ant-modal-close {
color: @color-text-secondary !important; color: @color-text-secondary !important;
transition: all 0.3s ease; transition: all 0.3s ease;
// 关闭按钮放在弹窗外侧右上角(弹窗右边外面)
top: 0 !important;
right: -50px !important;
inset-inline-end: -50px !important;
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.15) !important;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&:hover { &:hover {
color: @color-gold-primary !important; color: @color-gold-primary !important;
background: rgba(212, 175, 55, 0.1) !important; background: rgba(255, 255, 255, 0.25) !important;
} }
} }
} }
@@ -395,6 +406,121 @@
} }
} }
// ==================== Tab 切换样式 ====================
.auth-modal .tab-container {
display: flex;
justify-content: center;
align-items: center;
position: relative;
margin-bottom: 24px;
}
.auth-modal .tab-wrapper {
display: flex;
gap: 8px;
background: rgba(26, 26, 46, 0.6);
padding: 4px;
border-radius: 8px;
border: 1px solid @color-line-default;
}
.auth-modal .tab-btn {
padding: 8px 24px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
border: none;
background: transparent;
color: @color-text-secondary;
font-size: 14px;
font-weight: 500;
&:hover:not(.active) {
color: @color-gold-primary;
background: rgba(212, 175, 55, 0.1);
}
&.active {
background: @color-gold-gradient;
color: @color-bg-elevated;
font-weight: 600;
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.3);
}
}
.auth-modal .switch-icon {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
color: @color-text-secondary;
cursor: pointer;
padding: 8px;
border-radius: 6px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: @color-gold-primary;
background: rgba(212, 175, 55, 0.1);
}
}
// ==================== 底部分割线 ====================
.auth-modal .bottom-divider {
display: flex;
align-items: center;
justify-content: center;
margin-top: 24px;
gap: 12px;
}
.auth-modal .divider-line {
flex: 1;
height: 1px;
background: linear-gradient(to right, transparent, @color-line-default, transparent);
}
.auth-modal .other-login-wrapper {
display: flex;
align-items: center;
gap: 8px;
color: @color-text-muted;
font-size: 12px;
white-space: nowrap;
}
.auth-modal .other-login-icon {
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
&.phone {
color: @color-gold-primary;
&:hover {
background: rgba(212, 175, 55, 0.1);
}
}
&.wechat {
color: @color-wechat;
&:hover {
background: rgba(7, 193, 96, 0.1);
}
}
}
// ==================== 内容区域 ====================
.auth-modal .content-area {
min-height: 280px;
}
// ==================== 响应式适配 ==================== // ==================== 响应式适配 ====================
@media (max-width: 768px) { @media (max-width: 768px) {
.auth-modal.ant-modal { .auth-modal.ant-modal {
@@ -415,4 +541,9 @@
width: 200px; width: 200px;
height: 200px; height: 200px;
} }
.auth-modal .tab-btn {
padding: 6px 16px;
font-size: 13px;
}
} }

View File

@@ -65,15 +65,15 @@ export default function AuthModalManager() {
} }
}, [isAuthModalOpen]); }, [isAuthModalOpen]);
// 响应式宽度配置 // 响应式宽度配置 - 单列布局
const modalMaxW = useBreakpointValue( const modalMaxW = useBreakpointValue(
{ {
base: "90%", base: "90%",
sm: "90%", sm: "90%",
md: "700px", md: "450px",
lg: "700px" lg: "450px"
}, },
{ fallback: "700px", ssr: false } { fallback: "450px", ssr: false }
); );
// Ant Design 5.x Modal styles 属性 - 直接覆盖 CSS-in-JS 样式 // Ant Design 5.x Modal styles 属性 - 直接覆盖 CSS-in-JS 样式
@@ -111,15 +111,13 @@ export default function AuthModalManager() {
width={modalMaxW} width={modalMaxW}
centered centered
destroyOnHidden={true} destroyOnHidden={true}
maskClosable={false} maskClosable={true}
keyboard={true} keyboard={true}
zIndex={999} zIndex={999}
className="auth-modal" className="auth-modal"
rootClassName="auth-modal-root" rootClassName="auth-modal-root"
styles={modalStyles} styles={modalStyles}
closeIcon={ closable={false}
<span style={{ color: THEME.textSecondary, fontSize: '16px' }}></span>
}
> >
<AuthFormContent /> <AuthFormContent />
</Modal> </Modal>

View File

@@ -35,13 +35,13 @@ const styles = {
}, },
sendCodeBtnDisabled: { sendCodeBtnDisabled: {
background: 'transparent', background: 'transparent',
color: THEME.textMuted, color: THEME.goldPrimary,
border: `1px solid ${THEME.lineDefault}`, border: `1px solid ${THEME.lineEmphasis}`,
borderLeft: 'none', borderLeft: 'none',
minWidth: '120px', minWidth: '120px',
height: '44px', height: '44px',
borderRadius: '0 8px 8px 0', borderRadius: '0 8px 8px 0',
opacity: 0.5, opacity: 0.8,
}, },
}; };

View File

@@ -139,7 +139,7 @@ const getStatusColor = (status) => {
const getStatusText = (status) => STATUS_MESSAGES[status] || "点击按钮获取二维码"; const getStatusText = (status) => STATUS_MESSAGES[status] || "点击按钮获取二维码";
export default function WechatRegister() { export default function WechatRegister({ subtitle }) {
const { closeModal } = useAuthModal(); const { closeModal } = useAuthModal();
const { refreshSession } = useAuth(); const { refreshSession } = useAuth();
const authEvents = useAuthEvents({ component: 'WechatRegister', isMobile: false }); const authEvents = useAuthEvents({ component: 'WechatRegister', isMobile: false });
@@ -299,6 +299,9 @@ export default function WechatRegister() {
return ( return (
<div style={styles.container}> <div style={styles.container}>
<Title level={5} style={styles.title}>微信登陆</Title> <Title level={5} style={styles.title}>微信登陆</Title>
<Text style={{ display: 'block', textAlign: 'center', color: 'rgba(255, 255, 255, 0.5)', fontSize: '12px', marginBottom: '12px' }}>
未注册的微信号登录时将自动创建价值前沿账号
</Text>
<div ref={containerRef} style={styles.qrcodeContainer}> <div ref={containerRef} style={styles.qrcodeContainer}>
{wechatStatus === WECHAT_STATUS.WAITING ? ( {wechatStatus === WECHAT_STATUS.WAITING ? (