登陆修改

This commit is contained in:
孙佳艺
2026-06-16 13:16:01 +08:00
parent 1b15561e0a
commit 5afe837dec
5 changed files with 182 additions and 63 deletions
+5 -3
View File
@@ -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 }),
}; };
+2 -2
View File
@@ -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;
} }
+2
View File
@@ -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}
+171 -58
View File
@@ -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>