登陆修改
This commit is contained in:
@@ -14,6 +14,7 @@ interface RequestOptions {
|
|||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
encryptBody?: boolean;
|
encryptBody?: boolean;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
|
skipAuthRedirect?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert snake_case string to camelCase */
|
/** Convert snake_case string to camelCase */
|
||||||
@@ -110,7 +111,7 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
|||||||
// Handle error responses
|
// Handle error responses
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const msg = parsed?.detail || `请求失败 (${res.status})`;
|
const msg = parsed?.detail || `请求失败 (${res.status})`;
|
||||||
if (res.status === 401) {
|
if (res.status === 401 && !options.skipAuthRedirect) {
|
||||||
clearToken();
|
clearToken();
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
@@ -122,11 +123,12 @@ if (!res.ok) {
|
|||||||
|
|
||||||
// Convenience methods
|
// Convenience methods
|
||||||
export const api = {
|
export const api = {
|
||||||
get: <T>(path: string, options: boolean | { auth?: boolean; signal?: AbortSignal } = true) => {
|
get: <T>(path: string, options: boolean | { auth?: boolean; signal?: AbortSignal; skipAuthRedirect?: boolean } = true) => {
|
||||||
const opts = typeof options === 'boolean' ? { auth: options } : options;
|
const opts = typeof options === 'boolean' ? { auth: options } : options;
|
||||||
return apiRequest<T>(path, opts);
|
return apiRequest<T>(path, opts);
|
||||||
},
|
},
|
||||||
post: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'POST', body, auth }),
|
post: <T>(path: string, body?: unknown, auth = true, skipAuthRedirect = false) =>
|
||||||
|
apiRequest<T>(path, { method: 'POST', body, auth, skipAuthRedirect }),
|
||||||
put: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'PUT', body, auth }),
|
put: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'PUT', body, auth }),
|
||||||
delete: <T>(path: string, auth = true) => apiRequest<T>(path, { method: 'DELETE', auth }),
|
delete: <T>(path: string, auth = true) => apiRequest<T>(path, { method: 'DELETE', auth }),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true';
|
|||||||
// ── Auth ──────────────────────────────────────────────────
|
// ── Auth ──────────────────────────────────────────────────
|
||||||
export async function login(username: string, password: string, captchaToken?: string, rememberMe?: boolean): Promise<User> {
|
export async function login(username: string, password: string, captchaToken?: string, rememberMe?: boolean): Promise<User> {
|
||||||
if (USE_MOCK) return mock.mockLogin({ username, password });
|
if (USE_MOCK) return mock.mockLogin({ username, password });
|
||||||
const res = await api.post<{ accessToken: string; user: User }>('/auth/login', { username, password, captcha_token: captchaToken, remember_me: rememberMe || false }, false);
|
const res = await api.post<{ accessToken: string; user: User }>('/auth/login', { username, password, captcha_token: captchaToken, remember_me: rememberMe || false }, false, true);
|
||||||
setToken(res.accessToken);
|
setToken(res.accessToken);
|
||||||
return res.user;
|
return res.user;
|
||||||
}
|
}
|
||||||
export async function phonelogin(phone: string, code: string): Promise<User> {
|
export async function phonelogin(phone: string, code: string): Promise<User> {
|
||||||
// if (USE_MOCK) return mock.mockLogin({ username, password });
|
// if (USE_MOCK) return mock.mockLogin({ username, password });
|
||||||
const res = await api.post<{ accessToken: string; user: User }>('/auth/sms-login', { phone, code }, false);
|
const res = await api.post<{ accessToken: string; user: User }>('/auth/sms-login', { phone, code }, false, true);
|
||||||
setToken(res.accessToken);
|
setToken(res.accessToken);
|
||||||
return res.user;
|
return res.user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,6 +256,8 @@ function InitialInfo() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 下一步按钮点击处理
|
// 下一步按钮点击处理
|
||||||
const handleNextStep = (stepId: number) => {
|
const handleNextStep = (stepId: number) => {
|
||||||
// 获取引擎 ID 和视频参数
|
// 获取引擎 ID 和视频参数
|
||||||
|
|||||||
@@ -469,6 +469,8 @@ const GenerateConver: React.FC = () => {
|
|||||||
<div style={{ position: 'relative', aspectRatio: '1/1', background: '#f8fafc' }}>
|
<div style={{ position: 'relative', aspectRatio: '1/1', background: '#f8fafc' }}>
|
||||||
{item.finalVideoUrl ? (
|
{item.finalVideoUrl ? (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.finalVideoCoverUrl}`}
|
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.finalVideoCoverUrl}`}
|
||||||
alt={item.targetProjectName}
|
alt={item.targetProjectName}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const LoginPage: React.FC = () => {
|
|||||||
const [showSliderVerify, setShowSliderVerify] = useState(false);
|
const [showSliderVerify, setShowSliderVerify] = useState(false);
|
||||||
const [sliderVerified, setSliderVerified] = useState(false);
|
const [sliderVerified, setSliderVerified] = useState(false);
|
||||||
const [loginSliderVerified, setLoginSliderVerified] = useState(false);
|
const [loginSliderVerified, setLoginSliderVerified] = useState(false);
|
||||||
|
const [sliderKey, setSliderKey] = useState(0);
|
||||||
|
const [showResend, setShowResend] = useState(false);
|
||||||
|
const [loginShowResend, setLoginShowResend] = useState(false);
|
||||||
const [siteName, setSiteName] = useState('VideoGen.AI');
|
const [siteName, setSiteName] = useState('VideoGen.AI');
|
||||||
const [siteLogo, setSiteLogo] = useState('');
|
const [siteLogo, setSiteLogo] = useState('');
|
||||||
const [agreementUrl, setAgreementUrl] = useState('');
|
const [agreementUrl, setAgreementUrl] = useState('');
|
||||||
@@ -53,15 +56,16 @@ const LoginPage: React.FC = () => {
|
|||||||
if (!checkAgreed()) return;
|
if (!checkAgreed()) return;
|
||||||
try {
|
try {
|
||||||
const values = await pwdForm.validateFields();
|
const values = await pwdForm.validateFields();
|
||||||
setLoading(true);
|
// setLoading(true);
|
||||||
await login(values.phone, values.password, undefined, values.rememberMe);
|
await login(values.phone, values.password, undefined, values.rememberMe);
|
||||||
message.success('登录成功,欢迎回来');
|
message.success('登录成功,欢迎回来');
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
navigate('/projects');
|
navigate('/projects');
|
||||||
} catch {
|
} catch (error: any) {
|
||||||
message.error('登录失败');
|
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
|
||||||
|
message.error(errorMsg);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
// setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,8 +82,9 @@ const LoginPage: React.FC = () => {
|
|||||||
message.success('登录成功,欢迎回来');
|
message.success('登录成功,欢迎回来');
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
navigate('/projects');
|
navigate('/projects');
|
||||||
} catch {
|
} catch (error: any) {
|
||||||
message.error('登录失败');
|
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
|
||||||
|
message.error(errorMsg);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -98,17 +103,23 @@ const LoginPage: React.FC = () => {
|
|||||||
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '注册失败';
|
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '注册失败';
|
||||||
message.error(errorMsg);
|
message.error(errorMsg);
|
||||||
} finally {
|
} finally {
|
||||||
// setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startCountdown = (setter: React.Dispatch<React.SetStateAction<number>>) => {
|
const startCountdown = (setter: React.Dispatch<React.SetStateAction<number>>, isReg?: boolean) => {
|
||||||
setter(60);
|
setter(60);
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setter((c) => {
|
setter((c) => {
|
||||||
if (c <= 1) {
|
if (c <= 1) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
setShowSliderVerify(false);
|
setShowSliderVerify(false);
|
||||||
|
// 倒计时结束后,设置重新发送状态
|
||||||
|
if (isReg) {
|
||||||
|
setShowResend(true);
|
||||||
|
} else {
|
||||||
|
setLoginShowResend(true);
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return c - 1;
|
return c - 1;
|
||||||
@@ -154,8 +165,19 @@ const LoginPage: React.FC = () => {
|
|||||||
await sendSms(phone,mode2);
|
await sendSms(phone,mode2);
|
||||||
startCountdown(isReg ? setRegCountdown : setCountdown);
|
startCountdown(isReg ? setRegCountdown : setCountdown);
|
||||||
message.success('验证码已发送');
|
message.success('验证码已发送');
|
||||||
} catch {
|
} catch (error: any) {
|
||||||
message.error('发送失败');
|
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
|
||||||
|
message.error(errorMsg);
|
||||||
|
// 发送失败,重置验证状态、倒计时和滑块组件
|
||||||
|
if (mode === 'register') {
|
||||||
|
setSliderVerified(false);
|
||||||
|
setRegCountdown(0);
|
||||||
|
} else {
|
||||||
|
setLoginSliderVerified(false);
|
||||||
|
setCountdown(0);
|
||||||
|
}
|
||||||
|
// 通过更新 key 强制刷新滑块组件
|
||||||
|
setSliderKey(prev => prev + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,20 +199,27 @@ const LoginPage: React.FC = () => {
|
|||||||
// 设置对应的验证状态
|
// 设置对应的验证状态
|
||||||
if (mode === 'register') {
|
if (mode === 'register') {
|
||||||
setSliderVerified(true);
|
setSliderVerified(true);
|
||||||
startCountdown(setRegCountdown);
|
startCountdown(setRegCountdown, true);
|
||||||
} else {
|
} else {
|
||||||
setLoginSliderVerified(true);
|
setLoginSliderVerified(true);
|
||||||
startCountdown(setCountdown);
|
startCountdown(setCountdown, false);
|
||||||
}
|
}
|
||||||
message.success('验证码已发送');
|
message.success('验证码已发送');
|
||||||
} catch {
|
} catch (error: any) {
|
||||||
message.error('发送失败');
|
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
|
||||||
// 发送失败,重置滑动验证组件
|
message.error(errorMsg);
|
||||||
|
// 发送失败,重置验证状态、倒计时和滑块组件
|
||||||
if (mode === 'register') {
|
if (mode === 'register') {
|
||||||
setSliderVerified(false);
|
setSliderVerified(false);
|
||||||
|
setRegCountdown(0);
|
||||||
|
setShowResend(true);
|
||||||
} else {
|
} else {
|
||||||
setLoginSliderVerified(false);
|
setLoginSliderVerified(false);
|
||||||
|
setCountdown(0);
|
||||||
|
setLoginShowResend(true);
|
||||||
}
|
}
|
||||||
|
// 通过更新 key 强制刷新滑块组件
|
||||||
|
setSliderKey(prev => prev + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,8 +227,20 @@ const LoginPage: React.FC = () => {
|
|||||||
setTab(t);
|
setTab(t);
|
||||||
setMode(t);
|
setMode(t);
|
||||||
setCountdown(0);
|
setCountdown(0);
|
||||||
|
setRegCountdown(0);
|
||||||
setLoginSliderVerified(false);
|
setLoginSliderVerified(false);
|
||||||
|
setSliderVerified(false);
|
||||||
setShowSliderVerify(false);
|
setShowSliderVerify(false);
|
||||||
|
setShowResend(false);
|
||||||
|
setLoginShowResend(false);
|
||||||
|
// 刷新滑动验证组件
|
||||||
|
setSliderKey(prev => prev + 1);
|
||||||
|
// 清空当前模式相关的表单
|
||||||
|
if (t === 'password') {
|
||||||
|
pwdForm.resetFields();
|
||||||
|
} else {
|
||||||
|
phoneForm.resetFields();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
@@ -338,7 +379,7 @@ const LoginPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Password Login */}
|
{/* Password Login */}
|
||||||
{mode === 'password' && (
|
{mode === 'password' && (
|
||||||
<Form form={pwdForm} size="large" layout="vertical">
|
<Form form={pwdForm} size="large" layout="vertical" onFinish={handlePasswordLogin}>
|
||||||
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
|
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
|
||||||
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" style={inputStyle} />
|
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" style={inputStyle} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -349,7 +390,7 @@ const LoginPage: React.FC = () => {
|
|||||||
<Checkbox>记住我的登录状态</Checkbox>
|
<Checkbox>记住我的登录状态</Checkbox>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item style={{ marginBottom: 12 }}>
|
<Form.Item style={{ marginBottom: 12 }}>
|
||||||
<Button type="primary" onClick={handlePasswordLogin} loading={loading} block style={{
|
<Button type="primary" htmlType="submit" loading={loading} block style={{
|
||||||
height: 48, borderRadius: 10, fontSize: 16, fontWeight: 600,
|
height: 48, borderRadius: 10, fontSize: 16, fontWeight: 600,
|
||||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||||
border: 'none', boxShadow: '0 8px 24px rgba(99,102,241,0.25)',
|
border: 'none', boxShadow: '0 8px 24px rgba(99,102,241,0.25)',
|
||||||
@@ -366,30 +407,38 @@ const LoginPage: React.FC = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}>
|
<Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}>
|
||||||
<Space.Compact style={{ width: '100%' }}>
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
<Input prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入验证码" maxLength={6}
|
<Input
|
||||||
|
prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />}
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
maxLength={6}
|
||||||
|
disabled={!loginSliderVerified}
|
||||||
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
|
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
|
||||||
<Button disabled={countdown > 0}
|
<Button disabled={countdown > 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (loginSliderVerified) {
|
if (loginShowResend) {
|
||||||
handleSliderSuccess(true);
|
// 重新发送时,只刷新滑块,不启动倒计时
|
||||||
|
setLoginSliderVerified(false);
|
||||||
|
setShowSliderVerify(true);
|
||||||
|
setSliderKey(prev => prev + 1);
|
||||||
} else {
|
} else {
|
||||||
|
// 首次点击,调用发送验证码,等待滑块验证
|
||||||
handleSendCode(phoneForm.getFieldValue('phone'));
|
handleSendCode(phoneForm.getFieldValue('phone'));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
height: 48, borderRadius: '0 10px 10px 0',
|
height: 48, borderRadius: '0 10px 10px 0',
|
||||||
background: countdown > 0 || loginSliderVerified ? '#f1f5f9' : 'rgba(99,102,241,0.1)',
|
background: countdown > 0 ? '#f1f5f9' : (loginSliderVerified ? '#f1f5f9' : 'rgba(99,102,241,0.1)'),
|
||||||
border: '1.5px solid #e2e8f0', borderLeft: 'none',
|
border: '1.5px solid #e2e8f0', borderLeft: 'none',
|
||||||
color: countdown > 0 ? '#94a3b8' : '#6366f1',
|
color: countdown > 0 ? '#94a3b8' : '#6366f1',
|
||||||
fontWeight: 600, minWidth: 100,
|
fontWeight: 600, minWidth: 100,
|
||||||
}}>
|
}}>
|
||||||
{countdown > 0 ? `${countdown}s` : (loginSliderVerified ? '重新发送' : '获取验证码')}
|
{countdown > 0 ? `${countdown}s` : (loginShowResend ? '重新发送' : '获取验证码')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{/* 滑动验证 - 获取验证码后显示 */}
|
{/* 滑动验证 - 获取验证码后显示 */}
|
||||||
{showSliderVerify && (
|
{showSliderVerify && (
|
||||||
<Form.Item style={{ marginBottom: 12 }}>
|
<Form.Item style={{ marginBottom: 12 }} key={sliderKey}>
|
||||||
<SliderVerify
|
<SliderVerify
|
||||||
onSuccess={handleSliderSuccess}
|
onSuccess={handleSliderSuccess}
|
||||||
isVerified={loginSliderVerified}
|
isVerified={loginSliderVerified}
|
||||||
@@ -415,40 +464,49 @@ const LoginPage: React.FC = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="regCode" rules={[{ required: true, message: '请输入验证码' }]}>
|
<Form.Item name="regCode" rules={[{ required: true, message: '请输入验证码' }]}>
|
||||||
<Space.Compact style={{ width: '100%' }}>
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
<Input prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入验证码" maxLength={6}
|
<Input
|
||||||
|
prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />}
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
maxLength={6}
|
||||||
|
disabled={!sliderVerified}
|
||||||
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
|
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
|
||||||
<Button disabled={regCountdown > 0}
|
<Button disabled={regCountdown > 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (sliderVerified) {
|
if (showResend) {
|
||||||
handleSliderSuccess(true);
|
// 重新发送时,只刷新滑块,不启动倒计时
|
||||||
|
setSliderVerified(false);
|
||||||
|
setShowSliderVerify(true);
|
||||||
|
setSliderKey(prev => prev + 1);
|
||||||
} else {
|
} else {
|
||||||
|
// 首次点击,调用发送验证码,等待滑块验证
|
||||||
handleSendCode(regForm.getFieldValue('phone'), true);
|
handleSendCode(regForm.getFieldValue('phone'), true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
height: 48, borderRadius: '0 10px 10px 0',
|
height: 48, borderRadius: '0 10px 10px 0',
|
||||||
background: regCountdown > 0 || sliderVerified ? '#f1f5f9' : 'rgba(99,102,241,0.1)',
|
background: regCountdown > 0 ? '#f1f5f9' : (sliderVerified ? '#f1f5f9' : 'rgba(99,102,241,0.1)'),
|
||||||
border: '1.5px solid #e2e8f0', borderLeft: 'none',
|
border: '1.5px solid #e2e8f0', borderLeft: 'none',
|
||||||
color: regCountdown > 0 ? '#94a3b8' : '#6366f1',
|
color: regCountdown > 0 ? '#94a3b8' : '#6366f1',
|
||||||
fontWeight: 600, minWidth: 100,
|
fontWeight: 600, minWidth: 100,
|
||||||
}}>
|
}}>
|
||||||
{regCountdown > 0 ? `${regCountdown}s` : (sliderVerified ? '重新发送' : '获取验证码')}
|
{regCountdown > 0 ? `${regCountdown}s` : (showResend ? '重新发送' : '获取验证码')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="password" rules={[{ required: true, message: '请设置密码' }, { min: 6, message: '密码至少6位' }]}>
|
|
||||||
<Input.Password prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请设置密码(至少6位)" style={inputStyle} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{/* 滑动验证 - 获取验证码后显示 */}
|
{/* 滑动验证 - 获取验证码后显示 */}
|
||||||
{showSliderVerify && (
|
{showSliderVerify && (
|
||||||
<Form.Item style={{ marginBottom: 12 }}>
|
<Form.Item style={{ marginBottom: 12 }} key={sliderKey}>
|
||||||
<SliderVerify
|
<SliderVerify
|
||||||
onSuccess={handleSliderSuccess}
|
onSuccess={handleSliderSuccess}
|
||||||
isVerified={sliderVerified}
|
isVerified={sliderVerified}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
<Form.Item name="password" rules={[{ required: true, message: '请设置密码' }, { min: 6, message: '密码至少6位' }]}>
|
||||||
|
<Input.Password prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请设置密码(至少6位)" style={inputStyle} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Form.Item style={{ marginBottom: 12 }}>
|
<Form.Item style={{ marginBottom: 12 }}>
|
||||||
<Button type="primary" onClick={handleRegister} loading={loading} block style={{
|
<Button type="primary" onClick={handleRegister} loading={loading} block style={{
|
||||||
@@ -483,14 +541,30 @@ const LoginPage: React.FC = () => {
|
|||||||
{mode === 'register' ? (
|
{mode === 'register' ? (
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{ fontSize: 13, color: '#6366f1', cursor: 'pointer' }}
|
style={{ fontSize: 13, color: '#6366f1', cursor: 'pointer' }}
|
||||||
onClick={() => { setMode('password'); setTab('password'); }}
|
onClick={() => {
|
||||||
|
setMode('password');
|
||||||
|
setTab('password');
|
||||||
|
setShowResend(false);
|
||||||
|
setLoginShowResend(false);
|
||||||
|
setSliderVerified(false);
|
||||||
|
setShowSliderVerify(false);
|
||||||
|
setSliderKey(prev => prev + 1);
|
||||||
|
regForm.resetFields();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
已有账号?去登录
|
已有账号?去登录
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{ fontSize: 13, color: '#6366f1', cursor: 'pointer' }}
|
style={{ fontSize: 13, color: '#6366f1', cursor: 'pointer' }}
|
||||||
onClick={() => { setMode('register'); }}
|
onClick={() => {
|
||||||
|
setMode('register');
|
||||||
|
setShowResend(false);
|
||||||
|
setLoginShowResend(false);
|
||||||
|
setSliderVerified(false);
|
||||||
|
setShowSliderVerify(false);
|
||||||
|
setSliderKey(prev => prev + 1);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
没有账号?立即注册
|
没有账号?立即注册
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@@ -513,10 +587,36 @@ const SliderVerify: React.FC<{
|
|||||||
const positionRef = React.useRef(0);
|
const positionRef = React.useRef(0);
|
||||||
const successRef = React.useRef(false); // 防止重复触发成功回调
|
const successRef = React.useRef(false); // 防止重复触发成功回调
|
||||||
|
|
||||||
const containerWidth = 360; // 容器宽度
|
const [containerWidth, setContainerWidth] = React.useState(360); // 容器宽度,自适应
|
||||||
const sliderWidth = 60; // 滑块宽度
|
const sliderWidth = 50; // 滑块宽度
|
||||||
const maxPosition = containerWidth - sliderWidth;
|
const maxPosition = containerWidth - sliderWidth;
|
||||||
|
|
||||||
|
// 监听容器尺寸变化,使其自适应父容器宽度
|
||||||
|
React.useEffect(() => {
|
||||||
|
const updateWidth = () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const width = containerRef.current.offsetWidth;
|
||||||
|
if (width > 0) {
|
||||||
|
setContainerWidth(width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateWidth();
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(updateWidth);
|
||||||
|
if (containerRef.current) {
|
||||||
|
resizeObserver.observe(containerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateWidth);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
window.removeEventListener('resize', updateWidth);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const updatePosition = (x: number) => {
|
const updatePosition = (x: number) => {
|
||||||
// 如果已经成功,不再处理
|
// 如果已经成功,不再处理
|
||||||
if (successRef.current || isVerified) return;
|
if (successRef.current || isVerified) return;
|
||||||
@@ -591,28 +691,36 @@ const SliderVerify: React.FC<{
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
style={{
|
style={{
|
||||||
width: containerWidth,
|
width: '100%',
|
||||||
height: 44,
|
height: 50,
|
||||||
background: '#f8fafc',
|
background: '#ffffff',
|
||||||
borderRadius: 8,
|
borderRadius: 25,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
border: '1px solid #e2e8f0',
|
border: '1px solid rgba(99, 102, 241, 0.1)',
|
||||||
|
boxShadow: 'inset 0 2px 6px rgba(148, 163, 184, 0.15), 0 4px 12px rgba(0, 0, 0, 0.04)',
|
||||||
cursor: isVerified ? 'default' : 'pointer',
|
cursor: isVerified ? 'default' : 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
touchAction: 'none',
|
touchAction: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 已滑动部分背景 */}
|
{/* 已滑动部分背景 - 包含阴影弧度 */}
|
||||||
<div
|
<div
|
||||||
ref={trackRef}
|
ref={trackRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 0,
|
left: -2,
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: isVerified ? containerWidth : sliderWidth,
|
width: isVerified ? containerWidth + 4 : sliderWidth,
|
||||||
background: isVerified ? '#dcfce7' : '#e0e7ff',
|
background: isVerified
|
||||||
|
? 'linear-gradient(90deg, #dcfce7 0%, #bbf7d0 100%)'
|
||||||
|
: 'linear-gradient(90deg, rgba(224, 231, 255, 0.6) 0%, rgba(199, 210, 254, 0.8) 100%)',
|
||||||
|
borderRadius: isVerified ? 0 : '0 25px 25px 0',
|
||||||
|
boxShadow: isVerified
|
||||||
|
? 'none'
|
||||||
|
: '4px 0 12px rgba(99, 102, 241, 0.2), 2px 0 4px rgba(99, 102, 241, 0.1)',
|
||||||
|
transition: successRef.current ? 'all 0.3s ease' : 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -632,14 +740,15 @@ const SliderVerify: React.FC<{
|
|||||||
color: isVerified ? '#16a34a' : '#64748b',
|
color: isVerified ? '#16a34a' : '#64748b',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
letterSpacing: 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isVerified ? (
|
{isVerified ? (
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
已发送
|
验证成功
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
'滑动发送'
|
'滑动发送'
|
||||||
@@ -652,28 +761,32 @@ const SliderVerify: React.FC<{
|
|||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: isVerified ? maxPosition : 0,
|
left: isVerified ? maxPosition : 0,
|
||||||
top: 4,
|
top: 5,
|
||||||
width: sliderWidth - 8,
|
width: sliderWidth - 10,
|
||||||
height: 36,
|
height: 40,
|
||||||
background: isVerified ? '#22c55e' : '#fff',
|
background: isVerified
|
||||||
borderRadius: 6,
|
? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
|
||||||
|
: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||||
|
borderRadius: '50%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
boxShadow: isVerified
|
||||||
|
? '0 4px 12px rgba(34, 197, 94, 0.4), 0 0 0 4px rgba(34, 197, 94, 0.1)'
|
||||||
|
: '0 4px 12px rgba(99, 102, 241, 0.35), 0 0 0 4px rgba(255, 255, 255, 0.8)',
|
||||||
cursor: isVerified ? 'default' : 'grab',
|
cursor: isVerified ? 'default' : 'grab',
|
||||||
|
transition: successRef.current ? 'all 0.3s ease' : 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isVerified ? (
|
{isVerified ? (
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
) : (
|
) : (
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M18 8L22 12L18 16"></path>
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
<path d="M2 12h20"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user