new
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button } from 'antd';
|
||||
import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button, Avatar, Dropdown } from 'antd';
|
||||
import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined, SearchOutlined, UserOutlined, LogoutOutlined, WechatOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import ParticleBackground from './ParticleBackground';
|
||||
import LoginModal from './LoginModal';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const { Header, Content, Footer } = AntLayout;
|
||||
|
||||
@@ -12,6 +14,9 @@ const Layout = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [loginVisible, setLoginVisible] = useState(false);
|
||||
|
||||
const { user, login, logout } = useAuth();
|
||||
|
||||
// 全局监听并持久化 ref 参数
|
||||
useEffect(() => {
|
||||
@@ -22,6 +27,22 @@ const Layout = ({ children }) => {
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const userMenu = {
|
||||
items: [
|
||||
{
|
||||
key: 'logout',
|
||||
label: '退出登录',
|
||||
icon: <LogoutOutlined />,
|
||||
onClick: handleLogout
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: '/',
|
||||
@@ -43,14 +64,9 @@ const Layout = ({ children }) => {
|
||||
icon: <SearchOutlined />,
|
||||
label: '我的订单',
|
||||
},
|
||||
{
|
||||
key: 'more',
|
||||
label: '...',
|
||||
},
|
||||
];
|
||||
|
||||
const handleMenuClick = (key) => {
|
||||
if (key === 'more') return;
|
||||
navigate(key);
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
@@ -112,7 +128,7 @@ const Layout = ({ children }) => {
|
||||
</motion.div>
|
||||
|
||||
{/* Desktop Menu */}
|
||||
<div className="desktop-menu" style={{ display: 'none', flex: 1 }}>
|
||||
<div className="desktop-menu" style={{ display: 'none', flex: 1, justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="horizontal"
|
||||
@@ -124,13 +140,37 @@ const Layout = ({ children }) => {
|
||||
borderBottom: 'none',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
minWidth: '400px'
|
||||
minWidth: '400px',
|
||||
marginRight: '20px'
|
||||
}}
|
||||
/>
|
||||
|
||||
{user ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 15 }}>
|
||||
{/* 小程序图标状态 */}
|
||||
<WechatOutlined
|
||||
style={{
|
||||
fontSize: 24,
|
||||
color: user.openid && !user.openid.startsWith('web_') ? '#07c160' : '#666',
|
||||
cursor: 'help'
|
||||
}}
|
||||
title={user.openid && !user.openid.startsWith('web_') ? '已绑定微信小程序' : '未绑定微信小程序'}
|
||||
/>
|
||||
|
||||
<Dropdown menu={userMenu}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', color: '#fff' }}>
|
||||
<Avatar src={user.avatar_url} icon={<UserOutlined />} style={{ marginRight: 8 }} />
|
||||
<span>{user.nickname}</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="primary" onClick={() => setLoginVisible(true)}>登录</Button>
|
||||
)}
|
||||
</div>
|
||||
<style>{`
|
||||
@media (min-width: 768px) {
|
||||
.desktop-menu { display: block !important; }
|
||||
.desktop-menu { display: flex !important; }
|
||||
.mobile-menu-btn { display: none !important; }
|
||||
}
|
||||
`}</style>
|
||||
@@ -153,6 +193,17 @@ const Layout = ({ children }) => {
|
||||
open={mobileMenuOpen}
|
||||
styles={{ body: { padding: 0, background: '#111' }, header: { background: '#111', borderBottom: '1px solid #333' }, wrapper: { width: 250 } }}
|
||||
>
|
||||
<div style={{ padding: '20px', textAlign: 'center', borderBottom: '1px solid #333' }}>
|
||||
{user ? (
|
||||
<div style={{ color: '#fff' }}>
|
||||
<Avatar src={user.avatar_url} icon={<UserOutlined />} size="large" style={{ marginBottom: 10 }} />
|
||||
<div>{user.nickname}</div>
|
||||
<Button type="link" danger onClick={handleLogout} style={{ marginTop: 10 }}>退出登录</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="primary" block onClick={() => { setLoginVisible(true); setMobileMenuOpen(false); }}>登录 / 注册</Button>
|
||||
)}
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="vertical"
|
||||
@@ -163,6 +214,12 @@ const Layout = ({ children }) => {
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
<LoginModal
|
||||
visible={loginVisible}
|
||||
onClose={() => setLoginVisible(false)}
|
||||
onLoginSuccess={(userData) => login(userData)}
|
||||
/>
|
||||
|
||||
<Content style={{ marginTop: 72, padding: '40px 20px', overflowX: 'hidden' }}>
|
||||
<div style={{
|
||||
maxWidth: '1200px',
|
||||
|
||||
122
frontend/src/components/LoginModal.jsx
Normal file
122
frontend/src/components/LoginModal.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Form, Input, Button, message } from 'antd';
|
||||
import { UserOutlined, LockOutlined, MobileOutlined } from '@ant-design/icons';
|
||||
import { sendSms, phoneLogin } from '../api';
|
||||
|
||||
const LoginModal = ({ visible, onClose, onLoginSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
|
||||
const handleSendCode = async () => {
|
||||
try {
|
||||
const phone = form.getFieldValue('phone_number');
|
||||
if (!phone) {
|
||||
message.error('请输入手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的手机号校验
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
message.error('请输入有效的手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
await sendSms({ phone_number: phone });
|
||||
message.success('验证码已发送');
|
||||
|
||||
setCountdown(60);
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('发送失败: ' + (error.response?.data?.error || '网络错误'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await phoneLogin(values);
|
||||
|
||||
message.success('登录成功');
|
||||
onLoginSuccess(res.data);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('登录失败: ' + (error.response?.data?.error || '网络错误'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="用户登录 / 注册"
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
name="login_form"
|
||||
onFinish={handleSubmit}
|
||||
layout="vertical"
|
||||
style={{ marginTop: 20 }}
|
||||
>
|
||||
<Form.Item
|
||||
name="phone_number"
|
||||
rules={[{ required: true, message: '请输入手机号' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<MobileOutlined />}
|
||||
placeholder="手机号码"
|
||||
size="large"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="code"
|
||||
rules={[{ required: true, message: '请输入验证码' }]}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<Input
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="验证码"
|
||||
size="large"
|
||||
/>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={handleSendCode}
|
||||
disabled={countdown > 0}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : '获取验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block size="large" loading={loading}>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'center', color: '#999', fontSize: 12 }}>
|
||||
未注册的手机号验证后将自动创建账号<br/>
|
||||
已在小程序绑定的手机号将自动同步身份
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginModal;
|
||||
Reference in New Issue
Block a user