“修改用户名/验证码滑动发送提示”

This commit is contained in:
sjy
2026-07-14 17:23:28 +08:00
parent 9637e5ba79
commit 4ade6bb347
14 changed files with 411 additions and 214 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -28,8 +28,8 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-BoVQkhEX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
<script type="module" crossorigin src="/assets/index-nrhXZrQV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DviWdElm.css">
</head>
<body>
<div id="root"></div>
+5
View File
@@ -46,6 +46,11 @@ export async function changePassword(oldPwd: string, newPwd: string): Promise<vo
if (USE_MOCK) return;
await api.post('/auth/change-password', { old_password: oldPwd, new_password: newPwd });
}
export async function changeUsername(newUsername: string): Promise<{ message: string; user?: User }> {
return api.post<{ message: string; user?: User }>('/auth/change-username', { new_username: newUsername });
}
// ── Projects ──────────────────────────────────────────────
export async function getProjects(): Promise<Project[]> {
if (USE_MOCK) return mock.mockGetProjects();
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio, Drawer } from 'antd';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio, Drawer, Tabs } from 'antd';
import { QRCodeSVG } from 'qrcode.react';
import {
PlayCircleOutlined,
@@ -62,7 +62,7 @@ import {
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore';
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword } from '../../api';
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername } from '../../api';
import NotificationPopup from '../NotificationPopup';
import './AppLayout.css';
@@ -309,11 +309,12 @@ const GRADIENTS = [
const AppLayout: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { user, logout } = useAuthStore();
const { user, logout, refreshUser } = useAuthStore();
const [pwdModalOpen, setPwdModalOpen] = useState(false);
const [rechargeModalOpen, setRechargeModalOpen] = useState(false);
const [contactModalOpen, setContactModalOpen] = useState(false);
const [pwdForm] = Form.useForm();
const [usernameForm] = Form.useForm();
const [selectedPlan, setSelectedPlan] = useState<number | null>(null);
const [menuItems, setMenuItems] = useState<MenuConfig[]>([]);
const [rechargeOptions, setRechargeOptions] = useState<any[]>([]);
@@ -603,7 +604,7 @@ const AppLayout: React.FC = () => {
{ key: 'messages', icon: <BellOutlined />, label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` },
...(operationManualUrl ? [{ key: 'manual' as const, icon: <FileTextOutlined />, label: '操作手册' }] : []),
{ type: 'divider' as const },
{ key: 'changePwd', icon: <LockOutlined />, label: '修改密码' },
{ key: 'changePwd', icon: <LockOutlined />, label: '个人信息' },
{ type: 'divider' as const },
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
];
@@ -635,6 +636,40 @@ const AppLayout: React.FC = () => {
}
};
const handleChangeUsername = async () => {
try {
const values = await usernameForm.validateFields();
const result = await changeUsername(values.username);
message.success(result.message || '操作成功');
await refreshUser();
getUser().then((res: any) => {
const rc = res?.resourceCapacity;
if (rc) {
setResourceCapacity({
enabled: !!rc.enabled,
usedBytes: Number(rc.usedBytes) || 0,
totalBytes: Number(rc.totalBytes) || 0,
availableBytes: Number(rc.availableBytes) || 0,
usagePercent: Number(rc.usagePercent) || 0,
exceeded: !!rc.exceeded,
limitValue: rc.limitValue ?? '',
limitUnit: rc.limitUnit || 'GB',
});
}
}).catch(() => { });
setPwdModalOpen(false);
usernameForm.resetFields();
} catch (error: any) {
if (error?.response?.data?.message) {
message.error(error.response.data.message);
} else if (error?.response?.data?.detail) {
message.error(error.response.data.detail);
} else if (error?.message) {
message.error(error.message);
}
}
};
const handleLogout = () => {
logout();
navigate('/login');
@@ -1138,7 +1173,7 @@ const AppLayout: React.FC = () => {
<span className="mobile-menu-icon" style={{ color: '#0ea5e9' }}>
<LockOutlined />
</span>
<span className="mobile-menu-label" style={{ color: '#475569' }}></span>
<span className="mobile-menu-label" style={{ color: '#475569' }}></span>
</div>
<div style={{ height: 8 }} />
@@ -1171,28 +1206,46 @@ const AppLayout: React.FC = () => {
</div>
</Drawer>
<Modal title={<Space><LockOutlined /></Space>} open={pwdModalOpen}
onOk={handleChangePwd} onCancel={() => { setPwdModalOpen(false); pwdForm.resetFields(); }}
okText="确认修改" cancelText="取消" width={440}>
<Form form={pwdForm} layout="vertical" style={{ marginTop: 20 }}>
<Form.Item name="oldPwd" label="原密码" rules={[{ required: true, message: '请输入原密码' }]}>
<Input.Password placeholder="请输入原密码" size="large" prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
</Form.Item>
<Form.Item name="newPwd" label="新密码" rules={[{ required: true, message: '请输入新密码' }, { min: 6, message: '密码至少6位' }]}>
<Input.Password placeholder="请输入新密码(至少6位)" size="large" prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
</Form.Item>
<Form.Item name="confirmPwd" label="确认新密码" rules={[
{ required: true, message: '请再次输入新密码' },
({ getFieldValue }: any) => ({
validator(_: any, value: string) {
if (!value || getFieldValue('newPwd') === value) return Promise.resolve();
return Promise.reject(new Error('两次密码不一致'));
},
}),
]}>
<Input.Password placeholder="请再次输入新密码" size="large" prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
</Form.Item>
</Form>
<Modal title={<Space><SettingOutlined /></Space>} open={pwdModalOpen}
onCancel={() => { setPwdModalOpen(false); pwdForm.resetFields(); usernameForm.resetFields(); }}
width={440}
footer={null}>
<Tabs defaultActiveKey="profile">
<Tabs.TabPane tab="个人信息" key="profile">
<Form form={usernameForm} layout="vertical" style={{ marginTop: 20 }} onValuesChange={() => {}}>
<Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }, { min: 3, message: '用户名至少3位' }]}>
<Input placeholder="请输入用户名" size="large" prefix={<UserOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
</Form.Item>
<Form.Item>
<Button type="primary" size="large" onClick={handleChangeUsername} style={{ width: '100%' }}></Button>
</Form.Item>
</Form>
</Tabs.TabPane>
<Tabs.TabPane tab="修改密码" key="password">
<Form form={pwdForm} layout="vertical" style={{ marginTop: 20 }} onValuesChange={() => {}}>
<Form.Item name="oldPwd" label="原密码" rules={[{ required: true, message: '请输入原密码' }]}>
<Input.Password placeholder="请输入原密码" size="large" prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
</Form.Item>
<Form.Item name="newPwd" label="新密码" rules={[{ required: true, message: '请输入新密码' }, { min: 6, message: '密码至少6位' }]}>
<Input.Password placeholder="请输入新密码(至少6位)" size="large" prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
</Form.Item>
<Form.Item name="confirmPwd" label="确认新密码" rules={[
{ required: true, message: '请再次输入新密码' },
({ getFieldValue }: any) => ({
validator(_: any, value: string) {
if (!value || getFieldValue('newPwd') === value) return Promise.resolve();
return Promise.reject(new Error('两次密码不一致'));
},
}),
]}>
<Input.Password placeholder="请再次输入新密码" size="large" prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
</Form.Item>
<Form.Item>
<Button type="primary" size="large" onClick={handleChangePwd} style={{ width: '100%' }}></Button>
</Form.Item>
</Form>
</Tabs.TabPane>
</Tabs>
</Modal>
<Modal title={<Space><GiftOutlined /></Space>} open={rechargeModalOpen}
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Tag, Typography, Button } from 'antd';
import { Tag, Button } from 'antd';
import { BellOutlined, ThunderboltOutlined, GiftOutlined, StarOutlined, CloseOutlined } from '@ant-design/icons';
import { getNotifications, markNotificationRead } from '../api';
import { useAuthStore } from '../store/useAuthStore';
@@ -13,10 +13,25 @@ interface Notification {
createdAt: string;
}
const typeConfig: Record<string, { gradient: string; icon: React.ReactNode; label: string }> = {
system: { gradient: 'linear-gradient(135deg, #6366f1, #818cf8)', icon: <ThunderboltOutlined />, label: '系统通知' },
credit: { gradient: 'linear-gradient(135deg, #f59e0b, #fbbf24)', icon: <StarOutlined />, label: '积分通知' },
promo: { gradient: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', icon: <GiftOutlined />, label: '活动通知' },
const typeConfig: Record<string, { gradient: string; icon: React.ReactNode; label: string; glowColor: string }> = {
system: {
gradient: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
icon: <ThunderboltOutlined />,
label: '系统通知',
glowColor: 'rgba(99, 102, 241, 0.4)',
},
credit: {
gradient: 'linear-gradient(135deg, #f59e0b 0%, #f97316 50%, #ea580c 100%)',
icon: <StarOutlined />,
label: '积分通知',
glowColor: 'rgba(245, 158, 11, 0.4)',
},
promo: {
gradient: 'linear-gradient(135deg, #8b5cf6 0%, #ec4899 50%, #f43f5e 100%)',
icon: <GiftOutlined />,
label: '活动通知',
glowColor: 'rgba(139, 92, 246, 0.4)',
},
};
const NotificationPopup: React.FC = () => {
@@ -29,10 +44,8 @@ const NotificationPopup: React.FC = () => {
const fetchNotifications = useCallback(async () => {
try {
const result = await getNotifications(1, 20, false);
// 接口返回结构:{ credits: { balance: 4317.23 }, items: [...], total: N }
const data = result.items || [];
// 同步积分:result.credits.balance 与当前 user.credits 不一致时,直接替换
const newBalance = result?.credits?.balance;
if (typeof newBalance === 'number') {
const currentUser = useAuthStore.getState().user;
@@ -66,7 +79,6 @@ const NotificationPopup: React.FC = () => {
setVisible(false);
setNotifications([]);
setCurrentIndex(0);
// 刷新用户信息(包括积分)
try { await refreshUser(); } catch { /* ignore */ }
}
};
@@ -90,97 +102,193 @@ const NotificationPopup: React.FC = () => {
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(15,15,35,0.6)', backdropFilter: 'blur(8px)',
background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(6px)',
}} onClick={handleClose}>
<div onClick={e => e.stopPropagation()} style={{
width: 420, borderRadius: 20, overflow: 'hidden',
boxShadow: '0 32px 80px rgba(0,0,0,0.35)',
animation: 'slideUp 0.35s cubic-bezier(0.16,1,0.3,1)',
width: 420,
borderRadius: 24,
overflow: 'hidden',
boxShadow: '0 25px 80px rgba(0,0,0,0.15)',
animation: 'notificationPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)',
background: '#ffffff',
}}>
{/* Header */}
<div style={{
background: tc.gradient,
padding: '28px 28px 24px',
position: 'relative', overflow: 'hidden',
padding: '24px 28px 20px',
position: 'relative',
overflow: 'hidden',
}}>
<div style={{ position: 'absolute', right: -20, top: -20, width: 120, height: 120, borderRadius: '50%', background: 'rgba(255,255,255,0.1)' }} />
<div style={{ position: 'absolute', right: 50, bottom: -30, width: 80, height: 80, borderRadius: '50%', background: 'rgba(255,255,255,0.06)' }} />
<div style={{
position: 'absolute',
right: -50,
top: -50,
width: 200,
height: 200,
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 60%)',
filter: 'blur(30px)',
}} />
<div style={{
position: 'absolute',
right: 30,
top: 10,
width: 80,
height: 80,
borderRadius: '50%',
background: 'rgba(255,255,255,0.06)',
filter: 'blur(15px)',
}} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 48, height: 48, borderRadius: 14,
background: 'rgba(255,255,255,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
backdropFilter: 'blur(4px)', fontSize: 22, color: '#fff',
width: 44,
height: 44,
borderRadius: 14,
background: 'rgba(255,255,255,0.15)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 20,
color: '#fff',
}}>{tc.icon}</div>
<div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginBottom: 2 }}></div>
<div style={{ color: '#fff', fontSize: 17, fontWeight: 700 }}>{current.title}</div>
<div style={{
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
marginBottom: 4,
fontWeight: 500,
}}></div>
<div style={{
color: '#fff',
fontSize: 28,
fontWeight: 700,
letterSpacing: '-1px',
}}>{current.title}</div>
</div>
</div>
<div onClick={handleClose} style={{
width: 28, height: 28, borderRadius: 8,
background: 'rgba(255,255,255,0.15)', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'background 0.2s',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.25)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.15)'; }}
<button
onClick={handleClose}
style={{
width: 30,
height: 30,
borderRadius: '50%',
background: 'rgba(255,255,255,0.1)',
border: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease',
outline: 'none',
}}
onMouseEnter={e => {
e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
}}
onMouseLeave={e => {
e.currentTarget.style.background = 'rgba(255,255,255,0.1)';
}}
>
<CloseOutlined style={{ color: '#fff', fontSize: 12 }} />
</div>
<CloseOutlined style={{ color: '#fff', fontSize: 14 }} />
</button>
</div>
</div>
{/* Body */}
<div style={{ background: '#fff', padding: '24px 28px 28px' }}>
<div style={{
background: '#ffffff',
padding: '24px 28px 28px',
}}>
<div style={{
padding: '18px 20px', background: '#f8fafc', borderRadius: 14,
marginBottom: 20, border: '1px solid #f0f0f5',
padding: '18px 20px',
background: '#f8fafc',
borderRadius: 16,
marginBottom: 20,
border: '1px solid #f1f5f9',
}}>
<div style={{ fontSize: 14, color: '#334155', lineHeight: 1.8 }}>
<div style={{
fontSize: 14,
color: '#475569',
lineHeight: 1.7,
}}>
{current.content}
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Tag color={tc.gradient.includes('#6366f1') ? '#6366f1' : tc.gradient.includes('#f59e0b') ? '#f59e0b' : '#8b5cf6'}
style={{ borderRadius: 6, border: 'none', padding: '2px 10px', fontSize: 12 }}>
<Tag
color={tc.gradient.includes('#6366f1') ? '#6366f1' : tc.gradient.includes('#f59e0b') ? '#f59e0b' : '#8b5cf6'}
style={{
borderRadius: 6,
border: 'none',
padding: '3px 10px',
fontSize: 11,
fontWeight: 500,
background: '#f1f5f9',
color: '#64748b',
}}
>
{tc.label}
</Tag>
{notifications.length > 1 && (
<span style={{ fontSize: 12, color: '#94a3b8' }}>{currentIndex + 1} / {notifications.length}</span>
)}
</div>
<Button type="primary" onClick={handleAcknowledge} style={{
borderRadius: 10, fontWeight: 600, height: 38,
background: tc.gradient, border: 'none',
paddingLeft: 28, paddingRight: 28,
boxShadow: `0 6px 16px ${tc.gradient.includes('#6366f1') ? 'rgba(99,102,241,0.3)' : tc.gradient.includes('#f59e0b') ? 'rgba(245,158,11,0.3)' : 'rgba(139,92,246,0.3)'}`,
}}>
<Button
type="primary"
onClick={handleAcknowledge}
style={{
borderRadius: 12,
fontWeight: 600,
height: 40,
background: tc.gradient,
border: 'none',
paddingLeft: 28,
paddingRight: 28,
boxShadow: `0 6px 16px ${tc.glowColor}`,
transition: 'all 0.2s ease',
fontSize: 14,
}}
onMouseEnter={e => {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = `0 8px 20px ${tc.glowColor}`;
}}
onMouseLeave={e => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = `0 6px 16px ${tc.glowColor}`;
}}
>
</Button>
</div>
{/* Dots */}
{notifications.length > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', gap: 6, marginTop: 18 }}>
<div style={{ display: 'flex', justifyContent: 'center', gap: 6, marginTop: 20 }}>
{notifications.map((_, i) => (
<div key={i} style={{
width: i === currentIndex ? 22 : 6, height: 6, borderRadius: 3,
background: i === currentIndex ? (tc.gradient.includes('#6366f1') ? '#6366f1' : tc.gradient.includes('#f59e0b') ? '#f59e0b' : '#8b5cf6') : '#e2e8f0',
transition: 'all 0.3s ease',
}} />
<button
key={i}
onClick={() => setCurrentIndex(i)}
style={{
width: i === currentIndex ? 20 : 5,
height: 5,
borderRadius: 3,
background: i === currentIndex
? (tc.gradient.includes('#6366f1') ? '#6366f1' : tc.gradient.includes('#f59e0b') ? '#f59e0b' : '#8b5cf6')
: '#e2e8f0',
border: 'none',
cursor: 'pointer',
transition: 'all 0.3s ease',
outline: 'none',
}}
/>
))}
</div>
)}
</div>
</div>
<style>{`
@keyframes slideUp {
from { opacity: 0; transform: translateY(24px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
@keyframes notificationPop {
0% { opacity: 0; transform: translateY(20px) scale(0.96); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
`}</style>
</div>
@@ -139,6 +139,8 @@ const PrivatePortraitAssetUpload: React.FC<Props> = ({ projectId, open, onClose,
disabled: fileList.length === 0 || fileList.some((f) => uploadStatus[f.uid] !== 'uploaded'),
}}
>
<div style={{ maxHeight: 600, overflow: 'auto' ,marginTop:30}}>
<Space direction="vertical" style={{ width: '100%' }} size={14}>
<Upload
accept="image/*,video/*"
@@ -237,7 +239,7 @@ const PrivatePortraitAssetUpload: React.FC<Props> = ({ projectId, open, onClose,
</div>
)}
{importState === 'imported' && (
<span style={{ fontSize: 12, color: '#10b981' }}> </span>
<span style={{ fontSize: 12, color: '#10b981' }}> </span>
)}
{importState === 'error' && (
<span style={{ fontSize: 12, color: '#ef4444' }}> </span>
@@ -290,9 +292,10 @@ const PrivatePortraitAssetUpload: React.FC<Props> = ({ projectId, open, onClose,
<Button icon={<UploadOutlined />}></Button>
</Upload>
<div style={{ padding: 12, background: '#f8fafc', borderRadius: 12, color: '#64748b', fontSize: 13 }}>
{MIN_PRIVATE_VIDEO_DURATION}~{MAX_PRIVATE_VIDEO_DURATION} Active AI
{MIN_PRIVATE_VIDEO_DURATION}~{MAX_PRIVATE_VIDEO_DURATION} AI
</div>
</Space>
</div>
</Modal>
);
};
@@ -428,7 +428,7 @@ const VirtualMaterialPanel: React.FC = () => {
{projects.length === 0 ? (
<Empty description="暂无虚拟人像项目组" />
) : (
<Space direction="vertical" style={{ width: '100%' }} size={10}>
<Space orientation="vertical" style={{ width: '100%' }} size={10}>
{projects.map((project) => {
const active = selectedProjectId === project.id;
return (
@@ -582,7 +582,7 @@ const VirtualMaterialPanel: React.FC = () => {
</Modal>
<Modal
title="上传虚拟人像素材"
title="上传虚拟人像素材"
open={uploadOpen}
onCancel={() => setUploadOpen(false)}
onOk={handleUpload}
@@ -593,11 +593,12 @@ const VirtualMaterialPanel: React.FC = () => {
disabled: fileList.length === 0 || fileList.some((f) => uploadStatus[f.uid] !== 'uploaded'),
}}
>
<Space direction="vertical" style={{ width: '100%' }} size={14}>
<Upload
accept="image/*,video/*"
fileList={fileList}
multiple
<div style={{ maxHeight: 600, overflow: 'auto' ,marginTop:30}}>
<Space direction="vertical" style={{ width: '100%' }} size={14}>
<Upload
accept="image/*,video/*"
fileList={fileList}
multiple
beforeUpload={async (file) => {
const uid = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const isImage = file.type.startsWith('image/');
@@ -691,7 +692,7 @@ const VirtualMaterialPanel: React.FC = () => {
</div>
)}
{importState === 'imported' && (
<span style={{ fontSize: 12, color: '#10b981' }}> </span>
<span style={{ fontSize: 12, color: '#10b981' }}> </span>
)}
{importState === 'error' && (
<span style={{ fontSize: 12, color: '#ef4444' }}> </span>
@@ -744,9 +745,10 @@ const VirtualMaterialPanel: React.FC = () => {
<Button icon={<UploadOutlined />}></Button>
</Upload>
<div style={{ padding: 12, background: '#f8fafc', borderRadius: 12, color: '#64748b', fontSize: 13 }}>
{MIN_PRIVATE_VIDEO_DURATION}~{MAX_PRIVATE_VIDEO_DURATION} CreateAsset Active AI
{MIN_PRIVATE_VIDEO_DURATION}~{MAX_PRIVATE_VIDEO_DURATION} CreateAsset AI
</div>
</Space>
</Space>
</div>
</Modal>
<Modal title="素材预览" open={previewOpen} onCancel={() => setPreviewOpen(false)} footer={null} width={760} destroyOnHidden>
+2 -2
View File
@@ -2026,8 +2026,8 @@ const AIChatPage: React.FC = () => {
{/* 头部 - 显示对话标题和模型信息 */}
<div className="animate-fadeInUp" style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '16px 24px', borderRadius: 22,
background: 'rgb(255, 255, 255)',
padding: '16px 24px',
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)',
backdropFilter: 'blur(22px)',
border: '1px solid rgba(231, 234, 240, 0.82)',
// boxShadow: '0 16px 44px rgba(31, 41, 55, 0.06)',
@@ -465,9 +465,9 @@ const GenerateConver: React.FC = () => {
{/* 顶部标题栏 */}
<div className="animate-fadeInUp" style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '16px 24px', borderRadius: 16,
padding: '16px 24px',
paddingBottom: 0,
background: 'rgba(255,255,255,0.6)',
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(99, 102, 241, 0.08)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
+13
View File
@@ -400,3 +400,16 @@
font-size: 15px !important;
}
}
@keyframes sliderShake {
0%, 100% { transform: translateX(0); }
10% { transform: translateX(-8px); }
20% { transform: translateX(8px); }
30% { transform: translateX(-6px); }
40% { transform: translateX(6px); }
50% { transform: translateX(-4px); }
60% { transform: translateX(4px); }
70% { transform: translateX(-2px); }
80% { transform: translateX(2px); }
90% { transform: translateX(-1px); }
}
+16 -3
View File
@@ -27,6 +27,7 @@ const LoginPage: React.FC = () => {
const [sliderVerified, setSliderVerified] = useState(false);
const [loginSliderVerified, setLoginSliderVerified] = useState(false);
const [sliderKey, setSliderKey] = useState(0);
const [sliderShake, setSliderShake] = useState(false);
const [showResend, setShowResend] = useState(false);
const [loginShowResend, setLoginShowResend] = useState(false);
@@ -394,6 +395,9 @@ const LoginPage: React.FC = () => {
setLoginSliderVerified(false);
setShowSliderVerify(true);
setSliderKey(prev => prev + 1);
} else if (showSliderVerify && !loginSliderVerified) {
setSliderShake(true);
setTimeout(() => setSliderShake(false), 500);
} else {
handleSendCode(phoneForm.getFieldValue('phone'));
}
@@ -408,6 +412,7 @@ const LoginPage: React.FC = () => {
<SliderVerify
onSuccess={handleSliderSuccess}
isVerified={loginSliderVerified}
shake={sliderShake}
/>
</Form.Item>
)}
@@ -437,6 +442,9 @@ const LoginPage: React.FC = () => {
setSliderVerified(false);
setShowSliderVerify(true);
setSliderKey(prev => prev + 1);
} else if (showSliderVerify && !sliderVerified) {
setSliderShake(true);
setTimeout(() => setSliderShake(false), 500);
} else {
handleSendCode(regForm.getFieldValue('phone'), true);
}
@@ -451,6 +459,7 @@ const LoginPage: React.FC = () => {
<SliderVerify
onSuccess={handleSliderSuccess}
isVerified={sliderVerified}
shake={sliderShake}
/>
</Form.Item>
)}
@@ -526,7 +535,8 @@ const LoginPage: React.FC = () => {
const SliderVerify: React.FC<{
onSuccess: () => void;
isVerified: boolean;
}> = ({ onSuccess, isVerified }) => {
shake?: boolean;
}> = ({ onSuccess, isVerified, shake }) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const sliderRef = React.useRef<HTMLDivElement>(null);
const trackRef = React.useRef<HTMLDivElement>(null);
@@ -673,11 +683,14 @@ const SliderVerify: React.FC<{
borderRadius: 25,
position: 'relative',
overflow: 'hidden',
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)',
border: shake ? '2px solid #f59e0b' : '1px solid rgba(99, 102, 241, 0.1)',
boxShadow: shake
? '0 0 15px rgba(245, 158, 11, 0.4), inset 0 2px 6px rgba(148, 163, 184, 0.15)'
: 'inset 0 2px 6px rgba(148, 163, 184, 0.15), 0 4px 12px rgba(0, 0, 0, 0.04)',
cursor: isVerified ? 'default' : 'pointer',
userSelect: 'none',
touchAction: 'none',
animation: shake ? 'sliderShake 0.5s ease-in-out' : 'none',
}}
>
<div
+1 -1
View File
@@ -110,7 +110,7 @@ const ProjectsPage: React.FC = () => {
<div className="animate-fadeInUp" style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
marginBottom: 24, padding: '16px 24px', borderRadius: 16,
background: 'rgba(255,255,255,0.6)',
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(99, 102, 241, 0.08)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
+1 -1
View File
@@ -194,7 +194,7 @@ export default function VideoFrameExtractor() {
<div className="animate-fadeInUp" style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
marginBottom: 24, padding: '16px 24px', borderRadius: 16,
background: 'rgba(255,255,255,0.6)',
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(139, 92, 246, 0.04) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(99, 102, 241, 0.08)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,