登录验证
This commit is contained in:
@@ -20,6 +20,12 @@ export async function login(username: string, password: string, captchaToken?: s
|
||||
setToken(res.accessToken);
|
||||
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> {
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
export async function sendSms(phone: string, captchaToken?: string): Promise<void> {
|
||||
export async function sendSms(phone: string, scene: string): Promise<void> {
|
||||
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 }> {
|
||||
@@ -351,3 +357,10 @@ export async function deleteHistory(id: string): Promise<void> {
|
||||
export async function calculateCredits(): Promise<any[]> {
|
||||
return api.get('/credits/credit-ratios');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 获取验证码
|
||||
export async function getSendcode(phone: string): Promise<any> {
|
||||
return api.post('/sms/send', { phone });
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
|
||||
import {
|
||||
getParameters, createGenerationTask, getgen_list, getEngine, uploadImage,
|
||||
uploadVideo, getCreditRatios, deleteHistory,calculateCredits
|
||||
uploadVideo, getCreditRatios, deleteHistory, calculateCredits
|
||||
} from '../api';
|
||||
|
||||
import {
|
||||
@@ -119,6 +119,7 @@ const AIChatPage: React.FC = () => {
|
||||
const [previewVisible, setPreviewVisible] = useState<boolean>(false);
|
||||
const [previewUrl, setPreviewUrl] = useState<string>('');
|
||||
const [previewType, setPreviewType] = useState<'image' | 'video'>('image');
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
// 从URL中提取exp时间戳(支持相对路径和完整URL)
|
||||
const extractExpTimestamp = (url: string): number | null => {
|
||||
@@ -209,20 +210,48 @@ const AIChatPage: React.FC = () => {
|
||||
// 获取预估积分 - 根据引擎ID、类型和分辨率计算
|
||||
const getEstimatedCredits = (): number => {
|
||||
// 根据当前选择的引擎ID、类型和分辨率查找对应的积分配置
|
||||
const config = creditCalculationData.find((item: any) =>
|
||||
item.modelConfigId === countType &&
|
||||
let config: any = {};
|
||||
config = creditCalculationData.find((item: any) =>
|
||||
item.modelConfigId === countType &&
|
||||
item.genType === mediaType &&
|
||||
item.resolution === (mediaType === 'video' ? videoResolution : selectedResolution)
|
||||
);
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log(config);
|
||||
|
||||
|
||||
|
||||
|
||||
// 根据配置计算积分
|
||||
if (mediaType === 'video') {
|
||||
// 视频:(秒数 × perSecondCredits + baseCredits) × ratio
|
||||
@@ -315,7 +344,7 @@ const AIChatPage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
getEngine()
|
||||
.then((data: any) => {
|
||||
console.log('引擎', data);
|
||||
// console.log('引擎', data);
|
||||
|
||||
setEnginesele(data.engine);
|
||||
|
||||
@@ -345,18 +374,18 @@ const AIChatPage: React.FC = () => {
|
||||
});
|
||||
getCreditRatios()
|
||||
.then((data: any) => {
|
||||
console.log('积分', data);
|
||||
// console.log('积分', data);
|
||||
|
||||
setCreditRatios(data.video || []);
|
||||
setCimage(data.image || []);
|
||||
})
|
||||
.catch((error) => {
|
||||
});
|
||||
calculateCredits().then((data: any) => {
|
||||
console.log('积分计算', data);
|
||||
// 保存积分计算数据
|
||||
setCreditCalculationData(data);
|
||||
})
|
||||
calculateCredits().then((data: any) => {
|
||||
// console.log('积分计算', data);
|
||||
// 保存积分计算数据
|
||||
setCreditCalculationData(data);
|
||||
})
|
||||
getgen_list(Pagebreak).then((data: any) => {
|
||||
let mess_list = data.items
|
||||
let total = data.total
|
||||
@@ -775,6 +804,9 @@ const AIChatPage: React.FC = () => {
|
||||
const handleClosePreview = () => {
|
||||
setPreviewVisible(false);
|
||||
setPreviewUrl('');
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
@@ -975,218 +1007,289 @@ const AIChatPage: React.FC = () => {
|
||||
{(() => {
|
||||
const msgApi = message;
|
||||
return gen_list.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start', // 所有消息左对齐
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 10, maxWidth: '70%' }}>
|
||||
{/* 头像 */}
|
||||
<div
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 50,
|
||||
background: '#e0e0e0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<RobotOutlined style={{ color: '#666', fontSize: 16 }} />
|
||||
</div>
|
||||
|
||||
{/* 消息内容 */}
|
||||
<div>
|
||||
|
||||
{/* 消息气泡 */}
|
||||
<div
|
||||
key={msg.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start', // 所有消息左对齐
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 10, maxWidth: '70%' }}>
|
||||
{/* 头像 */}
|
||||
<div
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: '16px 16px 16px 4px',
|
||||
padding: '12px 16px',
|
||||
width: '600px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
|
||||
position: 'relative',
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 50,
|
||||
background: '#e0e0e0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{/* 删除按钮 - 右上角 */}
|
||||
<div style={{ position: 'absolute', top: 8, right: 8 }}>
|
||||
<Popconfirm
|
||||
title="确定要删除吗?"
|
||||
onConfirm={async () => {
|
||||
await deleteHistory(msg.id);
|
||||
msgApi.success('删除成功');
|
||||
// 直接在本地列表中删除对应数据
|
||||
setGen_list(prev => prev.filter(item => item.id !== msg.id));
|
||||
setTotalnumber(prev => prev - 1);
|
||||
}}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
<RobotOutlined style={{ color: '#666', fontSize: 16 }} />
|
||||
</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
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: '16px 16px 16px 4px',
|
||||
padding: '12px 16px',
|
||||
width: '600px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* 删除按钮 - 右上角 */}
|
||||
<div style={{ position: 'absolute', top: 8, right: 8 }}>
|
||||
<Popconfirm
|
||||
title="确定要删除吗?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await deleteHistory(msg.id);
|
||||
msgApi.success('删除成功');
|
||||
// 直接在本地列表中删除对应数据
|
||||
setGen_list(prev => prev.filter(item => item.id !== msg.id));
|
||||
setTotalnumber(prev => prev - 1);
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
|
||||
const errorMsg = error?.response?.data?.message || error?.message || '删除失败';
|
||||
msgApi.error(errorMsg);
|
||||
}
|
||||
}}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
border: 'none',
|
||||
background: 'rgba(0,0,0,0.05)',
|
||||
color: '#999',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s ease',
|
||||
padding: 0,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#fff2f0';
|
||||
e.currentTarget.style.color = '#ff4d4f';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(0,0,0,0.05)';
|
||||
e.currentTarget.style.color = '#999';
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined style={{ fontSize: 12 }} />
|
||||
</button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
|
||||
{/* 文本内容 */}
|
||||
<Tooltip
|
||||
title={msg.originalPrompt}
|
||||
placement="top"
|
||||
style={{ maxWidth: '400px' }}
|
||||
>
|
||||
<button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
border: 'none',
|
||||
background: 'rgba(0,0,0,0.05)',
|
||||
color: '#999',
|
||||
cursor: 'pointer',
|
||||
<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}
|
||||
</p>
|
||||
</Tooltip>
|
||||
|
||||
{/* 根据 status 显示不同内容 */}
|
||||
{/* 生成中 - 显示加载动画 */}
|
||||
{msg.status === 'generating' && (
|
||||
<div style={{ height: 200, marginBottom: 10, marginTop: 12, borderRadius: 8, overflow: 'hidden', position: 'relative', backgroundColor: '#f1f5f9' }}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s ease',
|
||||
padding: 0,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#fff2f0';
|
||||
e.currentTarget.style.color = '#ff4d4f';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(0,0,0,0.05)';
|
||||
e.currentTarget.style.color = '#999';
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined style={{ fontSize: 12 }} />
|
||||
</button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
|
||||
{/* 文本内容 */}
|
||||
<p style={{ margin: 0, fontSize: 14, color: '#333' }}>
|
||||
{msg.originalPrompt}
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
{/* 根据 status 显示不同内容 */}
|
||||
{/* 生成中 - 显示加载动画 */}
|
||||
{msg.status === 'generating' && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 16, marginTop: 12 }}>
|
||||
<div style={{ background: '#fff', borderRadius: '16px 16px 16px 4px', padding: '12px 16px', boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<span style={{ color: '#6366f1', fontSize: 14, fontWeight: 500 }}>正在生成中</span>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: 50, background: '#6366f1', animation: 'blink 1s infinite' }} />
|
||||
<span style={{ width: 8, height: 8, borderRadius: 50, background: '#6366f1', animation: 'blink 1s infinite 0.2s' }} />
|
||||
<span style={{ width: 8, height: 8, borderRadius: 50, background: '#6366f1', animation: 'blink 1s infinite 0.4s' }} />
|
||||
gap: 16
|
||||
}}>
|
||||
<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 style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent)',
|
||||
animation: 'shimmer 2s infinite'
|
||||
}} />
|
||||
</div>
|
||||
)}
|
||||
{/* 生成失败 - 显示失败提示 */}
|
||||
{msg.status === 'failed' && (
|
||||
<div style={{
|
||||
height: 200,
|
||||
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>
|
||||
)}
|
||||
{/* 生成失败 - 显示失败提示 */}
|
||||
{msg.status === 'failed' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10, marginTop: 12, padding: '12px', background: '#fff2f0', borderRadius: 8 }}>
|
||||
<WarningOutlined style={{ color: '#ff4d4f', fontSize: 16 }} />
|
||||
<span style={{ fontSize: 14, color: '#ff4d4f' }}>生成失败,请重试</span>
|
||||
</div>
|
||||
)}
|
||||
{/* 已完成 - 显示媒体内容 */}
|
||||
{msg.status === 'completed' && (
|
||||
<div style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 8, marginBottom: 10, marginTop: 12, width: '100%', }}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setPreviewUrl(msg.genType === 'image' ? msg.imageUrl : msg.videoUrl);
|
||||
setPreviewType(msg.genType === 'image' ? 'image' : 'video');
|
||||
setPreviewVisible(true);
|
||||
}}
|
||||
style={{ cursor: 'pointer', overflow: 'hidden', borderRadius: 8, position: 'relative', height: 200 }}
|
||||
>
|
||||
{msg.genType === 'image' ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${msg.imageUrl}&w=300&p=50`}
|
||||
alt={msg.name}
|
||||
style={{ width: '100%', height: '100%', borderRadius: 8, objectFit: 'contain', backgroundColor: '#f1f5f9', transition: 'transform 0.2s' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
)}
|
||||
{/* 已完成 - 显示媒体内容 */}
|
||||
{msg.status === 'completed' && (
|
||||
<div style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 8, marginBottom: 10, marginTop: 12, width: '100%', }}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setPreviewUrl(msg.genType === 'image' ? msg.imageUrl : msg.videoUrl);
|
||||
setPreviewType(msg.genType === 'image' ? 'image' : 'video');
|
||||
setPreviewVisible(true);
|
||||
}}
|
||||
style={{ cursor: 'pointer', overflow: 'hidden', borderRadius: 8, position: 'relative', height: 200 }}
|
||||
>
|
||||
{msg.genType === 'image' ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${msg.videoCoverUrl}&w=300&p=50`}
|
||||
alt={msg.name}
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${msg.imageUrl}&w=300&p=50`}
|
||||
alt={msg.name}
|
||||
style={{ width: '100%', height: '100%', borderRadius: 8, objectFit: 'contain', backgroundColor: '#f1f5f9', transition: 'transform 0.2s' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
|
||||
/>
|
||||
{/* 视频播放按钮 */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 56,
|
||||
height: 56,
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
||||
}}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#fff">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${msg.videoCoverUrl}&w=300&p=50`}
|
||||
alt={msg.name}
|
||||
style={{ width: '100%', height: '100%', borderRadius: 8, objectFit: 'contain', backgroundColor: '#f1f5f9', transition: 'transform 0.2s' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
|
||||
/>
|
||||
{/* 视频播放按钮 */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 56,
|
||||
height: 56,
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
||||
}}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#fff">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
</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>
|
||||
))})()}
|
||||
))
|
||||
})()}
|
||||
|
||||
|
||||
{/* 消息列表底部标记 - 用于自动滚动 */}
|
||||
@@ -1464,10 +1567,10 @@ const AIChatPage: React.FC = () => {
|
||||
<Select
|
||||
value={mediaType}
|
||||
onChange={(val) => setMediaType(val)}
|
||||
style={{
|
||||
width: 100,
|
||||
outline: 'none',
|
||||
background: '#f1f5f9',
|
||||
style={{
|
||||
width: 100,
|
||||
outline: 'none',
|
||||
background: '#f1f5f9',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
height: 34,
|
||||
@@ -2222,6 +2325,7 @@ const AIChatPage: React.FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewUrl}`}
|
||||
controls
|
||||
style={{ maxWidth: '100%', maxHeight: '400px' }}
|
||||
|
||||
@@ -7,17 +7,21 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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 LoginPage: React.FC = () => {
|
||||
const { checkAuth } = useAuthStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [mode, setMode] = useState<'password' | 'phone' | 'register'>('password');
|
||||
const [tab, setTab] = useState<'password' | 'phone'>('password');
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [regCountdown, setRegCountdown] = useState(0);
|
||||
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 [siteLogo, setSiteLogo] = useState('');
|
||||
const [agreementUrl, setAgreementUrl] = useState('');
|
||||
@@ -52,6 +56,7 @@ const LoginPage: React.FC = () => {
|
||||
setLoading(true);
|
||||
await login(values.phone, values.password, undefined, values.rememberMe);
|
||||
message.success('登录成功,欢迎回来');
|
||||
await checkAuth();
|
||||
navigate('/projects');
|
||||
} catch {
|
||||
message.error('登录失败');
|
||||
@@ -69,8 +74,9 @@ const LoginPage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
await login(values.phone, values.code);
|
||||
await phonelogin(values.phone, values.code);
|
||||
message.success('登录成功,欢迎回来');
|
||||
await checkAuth();
|
||||
navigate('/projects');
|
||||
} catch {
|
||||
message.error('登录失败');
|
||||
@@ -84,40 +90,116 @@ const LoginPage: React.FC = () => {
|
||||
try {
|
||||
const values = await regForm.validateFields();
|
||||
setLoading(true);
|
||||
await registerApi(values.phone, values.regCode, values.password);
|
||||
const user = await register(values.phone, values.regCode, values.password);
|
||||
message.success('注册成功');
|
||||
await checkAuth();
|
||||
navigate('/projects');
|
||||
} catch {
|
||||
message.error('注册失败');
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '注册失败';
|
||||
message.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startCountdown = (setter: React.Dispatch<React.SetStateAction<number>>) => {
|
||||
setter(60);
|
||||
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);
|
||||
};
|
||||
|
||||
// 登录模式倒计时结束后重置验证状态
|
||||
useEffect(() => {
|
||||
if (countdown === 0 && mode === 'phone') {
|
||||
setLoginSliderVerified(false);
|
||||
}
|
||||
}, [countdown, mode]);
|
||||
|
||||
const handleSendCode = async (phone: string, isReg?: boolean) => {
|
||||
try {
|
||||
if (!phone || !/^1\d{10}$/.test(phone)) {
|
||||
message.error('请输入正确的手机号');
|
||||
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);
|
||||
message.success('验证码已发送');
|
||||
} catch {
|
||||
message.error('发送失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 滑动验证成功后的回调
|
||||
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') => {
|
||||
setTab(t);
|
||||
setMode(t);
|
||||
setCountdown(0);
|
||||
setLoginSliderVerified(false);
|
||||
setShowSliderVerify(false);
|
||||
};
|
||||
|
||||
const features = [
|
||||
@@ -256,7 +338,7 @@ const LoginPage: React.FC = () => {
|
||||
|
||||
{/* Password Login */}
|
||||
{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: '请输入正确的手机号' }]}>
|
||||
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" style={inputStyle} />
|
||||
</Form.Item>
|
||||
@@ -267,7 +349,7 @@ const LoginPage: React.FC = () => {
|
||||
<Checkbox>记住我的登录状态</Checkbox>
|
||||
</Form.Item>
|
||||
<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,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none', boxShadow: '0 8px 24px rgba(99,102,241,0.25)',
|
||||
@@ -278,7 +360,7 @@ const LoginPage: React.FC = () => {
|
||||
|
||||
{/* Phone Login */}
|
||||
{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: '请输入正确的手机号' }]}>
|
||||
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" maxLength={11} style={inputStyle} />
|
||||
</Form.Item>
|
||||
@@ -287,20 +369,36 @@ const LoginPage: React.FC = () => {
|
||||
<Input prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入验证码" maxLength={6}
|
||||
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
|
||||
<Button disabled={countdown > 0}
|
||||
onClick={() => handleSendCode(phoneForm.getFieldValue('phone'))}
|
||||
onClick={() => {
|
||||
if (loginSliderVerified) {
|
||||
handleSliderSuccess(true);
|
||||
} else {
|
||||
handleSendCode(phoneForm.getFieldValue('phone'));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
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',
|
||||
color: countdown > 0 ? '#94a3b8' : '#6366f1',
|
||||
fontWeight: 600, minWidth: 100,
|
||||
}}>
|
||||
{countdown > 0 ? `${countdown}s` : '获取验证码'}
|
||||
{countdown > 0 ? `${countdown}s` : (loginSliderVerified ? '重新发送' : '获取验证码')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
{/* 滑动验证 - 获取验证码后显示 */}
|
||||
{showSliderVerify && (
|
||||
<Form.Item style={{ marginBottom: 12 }}>
|
||||
<SliderVerify
|
||||
onSuccess={handleSliderSuccess}
|
||||
isVerified={loginSliderVerified}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item style={{ marginBottom: 12 }}>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block style={{
|
||||
<Button type="primary" onClick={handlePhoneLogin} loading={loading} block style={{
|
||||
height: 48, borderRadius: 10, fontSize: 16, fontWeight: 600,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none', boxShadow: '0 8px 24px rgba(99,102,241,0.25)',
|
||||
@@ -311,7 +409,7 @@ const LoginPage: React.FC = () => {
|
||||
|
||||
{/* 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: '请输入正确的手机号' }]}>
|
||||
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" maxLength={11} style={inputStyle} />
|
||||
</Form.Item>
|
||||
@@ -320,24 +418,40 @@ const LoginPage: React.FC = () => {
|
||||
<Input prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入验证码" maxLength={6}
|
||||
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
|
||||
<Button disabled={regCountdown > 0}
|
||||
onClick={() => handleSendCode(regForm.getFieldValue('phone'), true)}
|
||||
onClick={() => {
|
||||
if (sliderVerified) {
|
||||
handleSliderSuccess(true);
|
||||
} else {
|
||||
handleSendCode(regForm.getFieldValue('phone'), true);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
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',
|
||||
color: regCountdown > 0 ? '#94a3b8' : '#6366f1',
|
||||
fontWeight: 600, minWidth: 100,
|
||||
}}>
|
||||
{regCountdown > 0 ? `${regCountdown}s` : '获取验证码'}
|
||||
{regCountdown > 0 ? `${regCountdown}s` : (sliderVerified ? '重新发送' : '获取验证码')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</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 && (
|
||||
<Form.Item style={{ marginBottom: 12 }}>
|
||||
<SliderVerify
|
||||
onSuccess={handleSliderSuccess}
|
||||
isVerified={sliderVerified}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item style={{ marginBottom: 12 }}>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block style={{
|
||||
<Button type="primary" onClick={handleRegister} loading={loading} block style={{
|
||||
height: 48, borderRadius: 10, fontSize: 16, fontWeight: 600,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user