登录验证
This commit is contained in:
@@ -20,6 +20,12 @@ export async function login(username: string, password: string, captchaToken?: s
|
|||||||
setToken(res.accessToken);
|
setToken(res.accessToken);
|
||||||
return res.user;
|
return res.user;
|
||||||
}
|
}
|
||||||
|
export async function phonelogin(phone: string, code: string): Promise<User> {
|
||||||
|
// if (USE_MOCK) return mock.mockLogin({ username, password });
|
||||||
|
const res = await api.post<{ accessToken: string; user: User }>('/auth/sms-login', { phone, code }, false);
|
||||||
|
setToken(res.accessToken);
|
||||||
|
return res.user;
|
||||||
|
}
|
||||||
|
|
||||||
export async function register(phone: string, code: string, password: string): Promise<User> {
|
export async function register(phone: string, code: string, password: string): Promise<User> {
|
||||||
const res = await api.post<{ accessToken: string; user: User }>('/auth/register', { phone, code, password }, false);
|
const res = await api.post<{ accessToken: string; user: User }>('/auth/register', { phone, code, password }, false);
|
||||||
@@ -211,9 +217,9 @@ export async function getParameters(): Promise<any[]> {
|
|||||||
|
|
||||||
// ── SMS ───────────────────────────────────────────────────
|
// ── SMS ───────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function sendSms(phone: string, captchaToken?: string): Promise<void> {
|
export async function sendSms(phone: string, scene: string): Promise<void> {
|
||||||
if (USE_MOCK) return;
|
if (USE_MOCK) return;
|
||||||
await api.post('/sms/send', { phone, captcha_token: captchaToken }, false);
|
await api.post('/sms/send', { phone, scene: scene }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifySms(phone: string, code: string): Promise<{ token: string }> {
|
export async function verifySms(phone: string, code: string): Promise<{ token: string }> {
|
||||||
@@ -351,3 +357,10 @@ export async function deleteHistory(id: string): Promise<void> {
|
|||||||
export async function calculateCredits(): Promise<any[]> {
|
export async function calculateCredits(): Promise<any[]> {
|
||||||
return api.get('/credits/credit-ratios');
|
return api.get('/credits/credit-ratios');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 获取验证码
|
||||||
|
export async function getSendcode(phone: string): Promise<any> {
|
||||||
|
return api.post('/sms/send', { phone });
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
const [previewVisible, setPreviewVisible] = useState<boolean>(false);
|
const [previewVisible, setPreviewVisible] = useState<boolean>(false);
|
||||||
const [previewUrl, setPreviewUrl] = useState<string>('');
|
const [previewUrl, setPreviewUrl] = useState<string>('');
|
||||||
const [previewType, setPreviewType] = useState<'image' | 'video'>('image');
|
const [previewType, setPreviewType] = useState<'image' | 'video'>('image');
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
// 从URL中提取exp时间戳(支持相对路径和完整URL)
|
// 从URL中提取exp时间戳(支持相对路径和完整URL)
|
||||||
const extractExpTimestamp = (url: string): number | null => {
|
const extractExpTimestamp = (url: string): number | null => {
|
||||||
@@ -209,14 +210,42 @@ const AIChatPage: React.FC = () => {
|
|||||||
// 获取预估积分 - 根据引擎ID、类型和分辨率计算
|
// 获取预估积分 - 根据引擎ID、类型和分辨率计算
|
||||||
const getEstimatedCredits = (): number => {
|
const getEstimatedCredits = (): number => {
|
||||||
// 根据当前选择的引擎ID、类型和分辨率查找对应的积分配置
|
// 根据当前选择的引擎ID、类型和分辨率查找对应的积分配置
|
||||||
const config = creditCalculationData.find((item: any) =>
|
let config: any = {};
|
||||||
|
config = creditCalculationData.find((item: any) =>
|
||||||
item.modelConfigId === countType &&
|
item.modelConfigId === countType &&
|
||||||
item.genType === mediaType &&
|
item.genType === mediaType &&
|
||||||
item.resolution === (mediaType === 'video' ? videoResolution : selectedResolution)
|
item.resolution === (mediaType === 'video' ? videoResolution : selectedResolution)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return 0;
|
if (mediaType === 'video') {
|
||||||
|
// 如果没有找到对应的配置,则使用默认配置
|
||||||
|
config = {
|
||||||
|
perSecondCredits: 2,
|
||||||
|
baseCredits: 60,
|
||||||
|
ratio: 1,
|
||||||
|
};
|
||||||
|
if (videoResolution === '1080p') {
|
||||||
|
config.ratio = 2;
|
||||||
|
} else if (videoResolution === '720p') {
|
||||||
|
config.ratio = 1.5;
|
||||||
|
} else if (videoResolution === '480p') {
|
||||||
|
config.ratio = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 如果没有找到对应的配置,则使用默认配置
|
||||||
|
config = {
|
||||||
|
perSecondCredits: 0.1,
|
||||||
|
baseCredits: 4,
|
||||||
|
ratio: 1,
|
||||||
|
};
|
||||||
|
if (selectedResolution === '2K') {
|
||||||
|
config.ratio = 1;
|
||||||
|
} else if (selectedResolution === '4K') {
|
||||||
|
config.ratio = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -315,7 +344,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getEngine()
|
getEngine()
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
console.log('引擎', data);
|
// console.log('引擎', data);
|
||||||
|
|
||||||
setEnginesele(data.engine);
|
setEnginesele(data.engine);
|
||||||
|
|
||||||
@@ -345,7 +374,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
getCreditRatios()
|
getCreditRatios()
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
console.log('积分', data);
|
// console.log('积分', data);
|
||||||
|
|
||||||
setCreditRatios(data.video || []);
|
setCreditRatios(data.video || []);
|
||||||
setCimage(data.image || []);
|
setCimage(data.image || []);
|
||||||
@@ -353,7 +382,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
});
|
});
|
||||||
calculateCredits().then((data: any) => {
|
calculateCredits().then((data: any) => {
|
||||||
console.log('积分计算', data);
|
// console.log('积分计算', data);
|
||||||
// 保存积分计算数据
|
// 保存积分计算数据
|
||||||
setCreditCalculationData(data);
|
setCreditCalculationData(data);
|
||||||
})
|
})
|
||||||
@@ -775,6 +804,9 @@ const AIChatPage: React.FC = () => {
|
|||||||
const handleClosePreview = () => {
|
const handleClosePreview = () => {
|
||||||
setPreviewVisible(false);
|
setPreviewVisible(false);
|
||||||
setPreviewUrl('');
|
setPreviewUrl('');
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.pause();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = (e: React.MouseEvent) => {
|
const handleDownload = (e: React.MouseEvent) => {
|
||||||
@@ -1002,7 +1034,44 @@ const AIChatPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* 消息内容 */}
|
{/* 消息内容 */}
|
||||||
<div>
|
<div>
|
||||||
|
{/* 时间戳和参数信息 */}
|
||||||
|
|
||||||
|
<div style={{ margin: 4, fontSize: 11, color: '#999', textAlign: 'left', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
||||||
|
<span>{msg.createdAt?.replace('T', ' ').split('.')[0]}</span>
|
||||||
|
{/* 引擎标签 */}
|
||||||
|
<span >
|
||||||
|
{/* <SettingsOutlined style={{ fontSize: 12 }} /> */}
|
||||||
|
{msg.engineSnapshot.name}
|
||||||
|
</span>
|
||||||
|
{/* 参数标签 */}
|
||||||
|
<span >
|
||||||
|
{/* <LayoutGridOutlined style={{ fontSize: 12 }} /> */}
|
||||||
|
{msg.genType === 'image'
|
||||||
|
? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}`
|
||||||
|
: `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}`
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span style={{ marginLeft: 8 }}>消耗积分:{msg.creditsCost}</span>
|
||||||
|
{msg.mediaReferences && msg.mediaReferences.length > 0 && (
|
||||||
|
<span
|
||||||
|
style={{ marginLeft: 20, color: '#6366f1', cursor: 'pointer' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const target = e.currentTarget as HTMLElement;
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
setAttachmentPopupPosition({
|
||||||
|
x: rect.left,
|
||||||
|
y: rect.top - 10
|
||||||
|
});
|
||||||
|
setAttachmentPopupMessageId(msg.id);
|
||||||
|
setAttachmentPopupVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
附件详情
|
||||||
|
</span>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{/* 消息气泡 */}
|
{/* 消息气泡 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -1019,11 +1088,18 @@ const AIChatPage: React.FC = () => {
|
|||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="确定要删除吗?"
|
title="确定要删除吗?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
await deleteHistory(msg.id);
|
await deleteHistory(msg.id);
|
||||||
msgApi.success('删除成功');
|
msgApi.success('删除成功');
|
||||||
// 直接在本地列表中删除对应数据
|
// 直接在本地列表中删除对应数据
|
||||||
setGen_list(prev => prev.filter(item => item.id !== msg.id));
|
setGen_list(prev => prev.filter(item => item.id !== msg.id));
|
||||||
setTotalnumber(prev => prev - 1);
|
setTotalnumber(prev => prev - 1);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
const errorMsg = error?.response?.data?.message || error?.message || '删除失败';
|
||||||
|
msgApi.error(errorMsg);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
okText="确定"
|
okText="确定"
|
||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
@@ -1059,33 +1135,96 @@ const AIChatPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 文本内容 */}
|
{/* 文本内容 */}
|
||||||
<p style={{ margin: 0, fontSize: 14, color: '#333' }}>
|
<Tooltip
|
||||||
|
title={msg.originalPrompt}
|
||||||
|
placement="top"
|
||||||
|
style={{ maxWidth: '400px' }}
|
||||||
|
>
|
||||||
|
<p style={{
|
||||||
|
margin: '8px 0',
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#475569',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
transition: 'background-color 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#f1f5f9'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }}
|
||||||
|
>
|
||||||
{msg.originalPrompt}
|
{msg.originalPrompt}
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{/* 根据 status 显示不同内容 */}
|
{/* 根据 status 显示不同内容 */}
|
||||||
{/* 生成中 - 显示加载动画 */}
|
{/* 生成中 - 显示加载动画 */}
|
||||||
{msg.status === 'generating' && (
|
{msg.status === 'generating' && (
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 16, marginTop: 12 }}>
|
<div style={{ height: 200, marginBottom: 10, marginTop: 12, borderRadius: 8, overflow: 'hidden', position: 'relative', backgroundColor: '#f1f5f9' }}>
|
||||||
<div style={{ background: '#fff', borderRadius: '16px 16px 16px 4px', padding: '12px 16px', boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
|
<div style={{
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
position: 'absolute',
|
||||||
<span style={{ color: '#6366f1', fontSize: 14, fontWeight: 500 }}>正在生成中</span>
|
inset: 0,
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
display: 'flex',
|
||||||
<span style={{ width: 8, height: 8, borderRadius: 50, background: '#6366f1', animation: 'blink 1s infinite' }} />
|
flexDirection: 'column',
|
||||||
<span style={{ width: 8, height: 8, borderRadius: 50, background: '#6366f1', animation: 'blink 1s infinite 0.2s' }} />
|
alignItems: 'center',
|
||||||
<span style={{ width: 8, height: 8, borderRadius: 50, background: '#6366f1', animation: 'blink 1s infinite 0.4s' }} />
|
justifyContent: 'center',
|
||||||
</div>
|
gap: 16
|
||||||
</div>
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
border: '3px solid #e2e8f0',
|
||||||
|
borderTopColor: '#6366f1',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite'
|
||||||
|
}} />
|
||||||
|
<span style={{ fontSize: 14, color: '#64748b' }}>正在生成中...</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent)',
|
||||||
|
animation: 'shimmer 2s infinite'
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 生成失败 - 显示失败提示 */}
|
{/* 生成失败 - 显示失败提示 */}
|
||||||
{msg.status === 'failed' && (
|
{msg.status === 'failed' && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10, marginTop: 12, padding: '12px', background: '#fff2f0', borderRadius: 8 }}>
|
<div style={{
|
||||||
<WarningOutlined style={{ color: '#ff4d4f', fontSize: 16 }} />
|
height: 200,
|
||||||
<span style={{ fontSize: 14, color: '#ff4d4f' }}>生成失败,请重试</span>
|
marginBottom: 10,
|
||||||
|
marginTop: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
border: '1px dashed #e2e8f0',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 12
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: '#fff2f0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<WarningOutlined style={{ color: '#ff4d4f', fontSize: 22 }} />
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: 14, color: '#94a3b8' }}>生成失败</span>
|
||||||
|
{/* <span style={{ fontSize: 12, color: '#cbd5e1' }}>请重试</span> */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 已完成 - 显示媒体内容 */}
|
{/* 已完成 - 显示媒体内容 */}
|
||||||
@@ -1143,50 +1282,14 @@ const AIChatPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{/* 时间戳和参数信息 */}
|
|
||||||
|
|
||||||
<div style={{ margin: 4, fontSize: 11, color: '#999', textAlign: 'left', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
|
||||||
<span>{msg.createdAt?.replace('T', ' ').split('.')[0]}</span>
|
|
||||||
{/* 引擎标签 */}
|
|
||||||
<span >
|
|
||||||
{/* <SettingsOutlined style={{ fontSize: 12 }} /> */}
|
|
||||||
{msg.engineSnapshot.name}
|
|
||||||
</span>
|
|
||||||
{/* 参数标签 */}
|
|
||||||
<span >
|
|
||||||
{/* <LayoutGridOutlined style={{ fontSize: 12 }} /> */}
|
|
||||||
{msg.genType === 'image'
|
|
||||||
? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}`
|
|
||||||
: `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}`
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
<span style={{ marginLeft: 8 }}>消耗积分:{msg.creditsCost}</span>
|
|
||||||
{msg.mediaReferences && msg.mediaReferences.length > 0 && (
|
|
||||||
<span
|
|
||||||
style={{ marginLeft: 20, color: '#6366f1', cursor: 'pointer' }}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const target = e.currentTarget as HTMLElement;
|
|
||||||
const rect = target.getBoundingClientRect();
|
|
||||||
setAttachmentPopupPosition({
|
|
||||||
x: rect.left,
|
|
||||||
y: rect.top - 10
|
|
||||||
});
|
|
||||||
setAttachmentPopupMessageId(msg.id);
|
|
||||||
setAttachmentPopupVisible(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
附件详情
|
|
||||||
</span>
|
|
||||||
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))})()}
|
))
|
||||||
|
})()}
|
||||||
|
|
||||||
|
|
||||||
{/* 消息列表底部标记 - 用于自动滚动 */}
|
{/* 消息列表底部标记 - 用于自动滚动 */}
|
||||||
@@ -2222,6 +2325,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<video
|
<video
|
||||||
|
ref={videoRef}
|
||||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewUrl}`}
|
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewUrl}`}
|
||||||
controls
|
controls
|
||||||
style={{ maxWidth: '100%', maxHeight: '400px' }}
|
style={{ maxWidth: '100%', maxHeight: '400px' }}
|
||||||
|
|||||||
@@ -7,17 +7,21 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../store/useAuthStore';
|
import { useAuthStore } from '../store/useAuthStore';
|
||||||
import { sendSms, getSiteInfo, register as registerApi } from '../api';
|
import { sendSms,phonelogin, getSiteInfo, register } from '../api';
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';
|
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';
|
||||||
|
|
||||||
const LoginPage: React.FC = () => {
|
const LoginPage: React.FC = () => {
|
||||||
|
const { checkAuth } = useAuthStore();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [mode, setMode] = useState<'password' | 'phone' | 'register'>('password');
|
const [mode, setMode] = useState<'password' | 'phone' | 'register'>('password');
|
||||||
const [tab, setTab] = useState<'password' | 'phone'>('password');
|
const [tab, setTab] = useState<'password' | 'phone'>('password');
|
||||||
const [countdown, setCountdown] = useState(0);
|
const [countdown, setCountdown] = useState(0);
|
||||||
const [regCountdown, setRegCountdown] = useState(0);
|
const [regCountdown, setRegCountdown] = useState(0);
|
||||||
const [agreed, setAgreed] = useState(false);
|
const [agreed, setAgreed] = useState(false);
|
||||||
|
const [showSliderVerify, setShowSliderVerify] = useState(false);
|
||||||
|
const [sliderVerified, setSliderVerified] = useState(false);
|
||||||
|
const [loginSliderVerified, setLoginSliderVerified] = 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('');
|
||||||
@@ -52,6 +56,7 @@ const LoginPage: React.FC = () => {
|
|||||||
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();
|
||||||
navigate('/projects');
|
navigate('/projects');
|
||||||
} catch {
|
} catch {
|
||||||
message.error('登录失败');
|
message.error('登录失败');
|
||||||
@@ -69,8 +74,9 @@ const LoginPage: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await login(values.phone, values.code);
|
await phonelogin(values.phone, values.code);
|
||||||
message.success('登录成功,欢迎回来');
|
message.success('登录成功,欢迎回来');
|
||||||
|
await checkAuth();
|
||||||
navigate('/projects');
|
navigate('/projects');
|
||||||
} catch {
|
} catch {
|
||||||
message.error('登录失败');
|
message.error('登录失败');
|
||||||
@@ -84,30 +90,68 @@ const LoginPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const values = await regForm.validateFields();
|
const values = await regForm.validateFields();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await registerApi(values.phone, values.regCode, values.password);
|
const user = await register(values.phone, values.regCode, values.password);
|
||||||
message.success('注册成功');
|
message.success('注册成功');
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startCountdown = (setter: React.Dispatch<React.SetStateAction<number>>) => {
|
const startCountdown = (setter: React.Dispatch<React.SetStateAction<number>>) => {
|
||||||
setter(60);
|
setter(60);
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setter((c) => { if (c <= 1) { clearInterval(timer); return 0; } return c - 1; });
|
setter((c) => {
|
||||||
|
if (c <= 1) {
|
||||||
|
clearInterval(timer);
|
||||||
|
setShowSliderVerify(false);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return c - 1;
|
||||||
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 登录模式倒计时结束后重置验证状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown === 0 && mode === 'phone') {
|
||||||
|
setLoginSliderVerified(false);
|
||||||
|
}
|
||||||
|
}, [countdown, mode]);
|
||||||
|
|
||||||
const handleSendCode = async (phone: string, isReg?: boolean) => {
|
const handleSendCode = async (phone: string, isReg?: boolean) => {
|
||||||
try {
|
try {
|
||||||
if (!phone || !/^1\d{10}$/.test(phone)) {
|
if (!phone || !/^1\d{10}$/.test(phone)) {
|
||||||
message.error('请输入正确的手机号');
|
message.error('请输入正确的手机号');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await sendSms(phone);
|
|
||||||
|
// 注册时需要滑动验证
|
||||||
|
if (isReg) {
|
||||||
|
setShowSliderVerify(true);
|
||||||
|
setSliderVerified(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录时也需要滑动验证
|
||||||
|
if (!loginSliderVerified) {
|
||||||
|
setShowSliderVerify(true);
|
||||||
|
setLoginSliderVerified(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode2 = '';
|
||||||
|
if (mode === 'register') {
|
||||||
|
mode2 = 'register';
|
||||||
|
} else {
|
||||||
|
mode2 = 'login';
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendSms(phone,mode2);
|
||||||
startCountdown(isReg ? setRegCountdown : setCountdown);
|
startCountdown(isReg ? setRegCountdown : setCountdown);
|
||||||
message.success('验证码已发送');
|
message.success('验证码已发送');
|
||||||
} catch {
|
} catch {
|
||||||
@@ -115,9 +159,47 @@ const LoginPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 滑动验证成功后的回调
|
||||||
|
const handleSliderSuccess = async (isResend = false) => {
|
||||||
|
// 根据当前模式获取手机号
|
||||||
|
const phone = mode === 'register' ? regForm.getFieldValue('phone') : phoneForm.getFieldValue('phone');
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
let mode2 = '';
|
||||||
|
if (mode === 'register') {
|
||||||
|
mode2 = 'register';
|
||||||
|
} else {
|
||||||
|
mode2 = 'login';
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendSms(phone,mode2);
|
||||||
|
// 设置对应的验证状态
|
||||||
|
if (mode === 'register') {
|
||||||
|
setSliderVerified(true);
|
||||||
|
startCountdown(setRegCountdown);
|
||||||
|
} else {
|
||||||
|
setLoginSliderVerified(true);
|
||||||
|
startCountdown(setCountdown);
|
||||||
|
}
|
||||||
|
message.success('验证码已发送');
|
||||||
|
} catch {
|
||||||
|
message.error('发送失败');
|
||||||
|
// 发送失败,重置滑动验证组件
|
||||||
|
if (mode === 'register') {
|
||||||
|
setSliderVerified(false);
|
||||||
|
} else {
|
||||||
|
setLoginSliderVerified(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const switchTab = (t: 'password' | 'phone') => {
|
const switchTab = (t: 'password' | 'phone') => {
|
||||||
setTab(t);
|
setTab(t);
|
||||||
setMode(t);
|
setMode(t);
|
||||||
|
setCountdown(0);
|
||||||
|
setLoginSliderVerified(false);
|
||||||
|
setShowSliderVerify(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
@@ -256,7 +338,7 @@ const LoginPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Password Login */}
|
{/* Password Login */}
|
||||||
{mode === 'password' && (
|
{mode === 'password' && (
|
||||||
<Form form={pwdForm} onFinish={handlePasswordLogin} size="large" layout="vertical">
|
<Form form={pwdForm} size="large" layout="vertical">
|
||||||
<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>
|
||||||
@@ -267,7 +349,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" htmlType="submit" loading={loading} block style={{
|
<Button type="primary" onClick={handlePasswordLogin} 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)',
|
||||||
@@ -278,7 +360,7 @@ const LoginPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Phone Login */}
|
{/* Phone Login */}
|
||||||
{mode === 'phone' && (
|
{mode === 'phone' && (
|
||||||
<Form form={phoneForm} onFinish={handlePhoneLogin} size="large" layout="vertical">
|
<Form form={phoneForm} size="large" layout="vertical">
|
||||||
<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="请输入手机号" maxLength={11} style={inputStyle} />
|
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" maxLength={11} style={inputStyle} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -287,20 +369,36 @@ const LoginPage: React.FC = () => {
|
|||||||
<Input prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入验证码" maxLength={6}
|
<Input prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入验证码" maxLength={6}
|
||||||
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={() => handleSendCode(phoneForm.getFieldValue('phone'))}
|
onClick={() => {
|
||||||
|
if (loginSliderVerified) {
|
||||||
|
handleSliderSuccess(true);
|
||||||
|
} else {
|
||||||
|
handleSendCode(phoneForm.getFieldValue('phone'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
height: 48, borderRadius: '0 10px 10px 0',
|
height: 48, borderRadius: '0 10px 10px 0',
|
||||||
background: countdown > 0 ? '#f1f5f9' : 'rgba(99,102,241,0.1)',
|
background: countdown > 0 || 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` : '获取验证码'}
|
{countdown > 0 ? `${countdown}s` : (loginSliderVerified ? '重新发送' : '获取验证码')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{/* 滑动验证 - 获取验证码后显示 */}
|
||||||
|
{showSliderVerify && (
|
||||||
<Form.Item style={{ marginBottom: 12 }}>
|
<Form.Item style={{ marginBottom: 12 }}>
|
||||||
<Button type="primary" htmlType="submit" loading={loading} block style={{
|
<SliderVerify
|
||||||
|
onSuccess={handleSliderSuccess}
|
||||||
|
isVerified={loginSliderVerified}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: 12 }}>
|
||||||
|
<Button type="primary" onClick={handlePhoneLogin} 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)',
|
||||||
@@ -311,7 +409,7 @@ const LoginPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Register */}
|
{/* Register */}
|
||||||
{mode === 'register' && (
|
{mode === 'register' && (
|
||||||
<Form form={regForm} onFinish={handleRegister} size="large" layout="vertical">
|
<Form form={regForm} size="large" layout="vertical">
|
||||||
<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="请输入手机号" maxLength={11} style={inputStyle} />
|
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" maxLength={11} style={inputStyle} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -320,24 +418,40 @@ const LoginPage: React.FC = () => {
|
|||||||
<Input prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入验证码" maxLength={6}
|
<Input prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入验证码" maxLength={6}
|
||||||
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={() => handleSendCode(regForm.getFieldValue('phone'), true)}
|
onClick={() => {
|
||||||
|
if (sliderVerified) {
|
||||||
|
handleSliderSuccess(true);
|
||||||
|
} else {
|
||||||
|
handleSendCode(regForm.getFieldValue('phone'), true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
height: 48, borderRadius: '0 10px 10px 0',
|
height: 48, borderRadius: '0 10px 10px 0',
|
||||||
background: regCountdown > 0 ? '#f1f5f9' : 'rgba(99,102,241,0.1)',
|
background: regCountdown > 0 || 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` : '获取验证码'}
|
{regCountdown > 0 ? `${regCountdown}s` : (sliderVerified ? '重新发送' : '获取验证码')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="password" rules={[{ required: true, message: '请设置密码' }, { min: 6, message: '密码至少6位' }]}>
|
<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} />
|
<Input.Password prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请设置密码(至少6位)" style={inputStyle} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 滑动验证 - 获取验证码后显示 */}
|
||||||
|
{showSliderVerify && (
|
||||||
<Form.Item style={{ marginBottom: 12 }}>
|
<Form.Item style={{ marginBottom: 12 }}>
|
||||||
<Button type="primary" htmlType="submit" loading={loading} block style={{
|
<SliderVerify
|
||||||
|
onSuccess={handleSliderSuccess}
|
||||||
|
isVerified={sliderVerified}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: 12 }}>
|
||||||
|
<Button type="primary" onClick={handleRegister} 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)',
|
||||||
@@ -388,4 +502,183 @@ const LoginPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 滑动验证组件
|
||||||
|
const SliderVerify: React.FC<{
|
||||||
|
onSuccess: () => void;
|
||||||
|
isVerified: boolean;
|
||||||
|
}> = ({ onSuccess, isVerified }) => {
|
||||||
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const sliderRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const trackRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const positionRef = React.useRef(0);
|
||||||
|
const successRef = React.useRef(false); // 防止重复触发成功回调
|
||||||
|
|
||||||
|
const containerWidth = 360; // 容器宽度
|
||||||
|
const sliderWidth = 60; // 滑块宽度
|
||||||
|
const maxPosition = containerWidth - sliderWidth;
|
||||||
|
|
||||||
|
const updatePosition = (x: number) => {
|
||||||
|
// 如果已经成功,不再处理
|
||||||
|
if (successRef.current || isVerified) return;
|
||||||
|
|
||||||
|
const newPosition = Math.max(0, Math.min(x, maxPosition));
|
||||||
|
positionRef.current = newPosition;
|
||||||
|
|
||||||
|
// 直接操作DOM,避免React状态更新的开销
|
||||||
|
if (sliderRef.current) {
|
||||||
|
sliderRef.current.style.left = `${newPosition}px`;
|
||||||
|
}
|
||||||
|
if (trackRef.current) {
|
||||||
|
trackRef.current.style.width = `${newPosition + sliderWidth}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证成功
|
||||||
|
if (newPosition >= maxPosition - 5) {
|
||||||
|
// 设置成功标志,防止重复触发
|
||||||
|
successRef.current = true;
|
||||||
|
|
||||||
|
// 设置验证成功状态
|
||||||
|
if (sliderRef.current) {
|
||||||
|
sliderRef.current.style.background = '#22c55e';
|
||||||
|
sliderRef.current.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
|
||||||
|
}
|
||||||
|
if (trackRef.current) {
|
||||||
|
trackRef.current.style.width = `${containerWidth}px`;
|
||||||
|
trackRef.current.style.background = '#dcfce7';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用成功回调
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (successRef.current || isVerified) return;
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
|
const x = moveEvent.clientX - rect.left - sliderWidth / 2;
|
||||||
|
updatePosition(x);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
|
||||||
|
// 如果没有滑到终点,重置位置
|
||||||
|
if (!successRef.current && positionRef.current < maxPosition - 5) {
|
||||||
|
positionRef.current = 0;
|
||||||
|
if (sliderRef.current) {
|
||||||
|
sliderRef.current.style.left = '0px';
|
||||||
|
}
|
||||||
|
if (trackRef.current) {
|
||||||
|
trackRef.current.style.width = `${sliderWidth}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
style={{
|
||||||
|
width: containerWidth,
|
||||||
|
height: 44,
|
||||||
|
background: '#f8fafc',
|
||||||
|
borderRadius: 8,
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: '1px solid #e2e8f0',
|
||||||
|
cursor: isVerified ? 'default' : 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
touchAction: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 已滑动部分背景 */}
|
||||||
|
<div
|
||||||
|
ref={trackRef}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: isVerified ? containerWidth : sliderWidth,
|
||||||
|
background: isVerified ? '#dcfce7' : '#e0e7ff',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 文字提示 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: isVerified ? '#16a34a' : '#64748b',
|
||||||
|
zIndex: 1,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isVerified ? (
|
||||||
|
<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">
|
||||||
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
已发送
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'滑动发送'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 滑块 */}
|
||||||
|
<div
|
||||||
|
ref={sliderRef}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: isVerified ? maxPosition : 0,
|
||||||
|
top: 4,
|
||||||
|
width: sliderWidth - 8,
|
||||||
|
height: 36,
|
||||||
|
background: isVerified ? '#22c55e' : '#fff',
|
||||||
|
borderRadius: 6,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||||
|
cursor: isVerified ? 'default' : 'grab',
|
||||||
|
userSelect: 'none',
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isVerified ? (
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M18 8L22 12L18 16"></path>
|
||||||
|
<path d="M2 12h20"></path>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default LoginPage;
|
export default LoginPage;
|
||||||
|
|||||||
Reference in New Issue
Block a user