“修改用户名/验证码滑动发送提示”
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+105
-105
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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>
|
||||
|
||||
@@ -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,10 +1206,23 @@ 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 }}>
|
||||
<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>
|
||||
@@ -1192,7 +1240,12 @@ const AppLayout: React.FC = () => {
|
||||
]}>
|
||||
<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={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', position: 'relative' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<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',
|
||||
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: 12 }}>
|
||||
<div style={{
|
||||
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',
|
||||
<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)';
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.25)'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.15)'; }}
|
||||
>
|
||||
<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={{
|
||||
padding: '18px 20px', background: '#f8fafc', borderRadius: 14,
|
||||
marginBottom: 20, border: '1px solid #f0f0f5',
|
||||
background: '#ffffff',
|
||||
padding: '24px 28px 28px',
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '18px 20px',
|
||||
background: '#f8fafc',
|
||||
borderRadius: 16,
|
||||
marginBottom: 20,
|
||||
border: '1px solid #f1f5f9',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 14,
|
||||
color: '#475569',
|
||||
lineHeight: 1.7,
|
||||
}}>
|
||||
<div style={{ fontSize: 14, color: '#334155', lineHeight: 1.8 }}>
|
||||
{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',
|
||||
<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 (
|
||||
@@ -593,6 +593,7 @@ const VirtualMaterialPanel: React.FC = () => {
|
||||
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/*"
|
||||
@@ -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>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal title="素材预览" open={previewOpen} onCancel={() => setPreviewOpen(false)} footer={null} width={760} destroyOnHidden>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user