1、修改前台左侧菜单显示
2、调整积分放在用户浮窗 3、消息改为页面列表显示 4、增加订单详情页面 5、调整新增页面的整体接口逻辑和风格
This commit is contained in:
@@ -24,10 +24,39 @@ import {
|
||||
CloseOutlined,
|
||||
WechatOutlined,
|
||||
AlipayCircleOutlined,
|
||||
SettingOutlined,
|
||||
AppstoreOutlined,
|
||||
FileTextOutlined,
|
||||
StarOutlined,
|
||||
HeartOutlined,
|
||||
CameraOutlined,
|
||||
CalculatorOutlined,
|
||||
DollarOutlined,
|
||||
FireOutlined,
|
||||
CloudOutlined,
|
||||
SmileOutlined,
|
||||
TrophyOutlined,
|
||||
RocketOutlined,
|
||||
BulbOutlined,
|
||||
PictureOutlined,
|
||||
VideoCameraOutlined,
|
||||
AudioOutlined,
|
||||
MailOutlined,
|
||||
PhoneOutlined,
|
||||
GlobalOutlined,
|
||||
ShoppingCartOutlined,
|
||||
TeamOutlined,
|
||||
BarChartOutlined,
|
||||
PieChartOutlined,
|
||||
LineChartOutlined,
|
||||
SecurityScanOutlined,
|
||||
ApiOutlined,
|
||||
DatabaseOutlined,
|
||||
CloudServerOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
|
||||
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount } from '../../api';
|
||||
import NotificationPopup from '../NotificationPopup';
|
||||
|
||||
interface MenuConfig {
|
||||
@@ -46,18 +75,44 @@ interface MenuConfig {
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
HomeOutlined: <HomeOutlined />,
|
||||
DashboardOutlined:<DashboardOutlined/>,
|
||||
CodeOutlined:<CodeOutlined/>,
|
||||
DashboardOutlined: <DashboardOutlined />,
|
||||
CodeOutlined: <CodeOutlined />,
|
||||
PlayCircleOutlined: <PlayCircleOutlined />,
|
||||
WalletOutlined: <WalletOutlined />,
|
||||
SettingOutlined: <LockOutlined />,
|
||||
BellOutlined: <GiftOutlined />,
|
||||
SettingOutlined: <SettingOutlined />,
|
||||
BellOutlined: <BellOutlined />,
|
||||
UserOutlined: <UserOutlined />,
|
||||
AppstoreOutlined: <FireFilled />,
|
||||
FileTextOutlined: <FireFilled />,
|
||||
StarOutlined: <StarFilled />,
|
||||
HeartOutlined: <FireFilled />,
|
||||
CameraOutlined: <PlayCircleOutlined />,
|
||||
AppstoreOutlined: <AppstoreOutlined />,
|
||||
FileTextOutlined: <FileTextOutlined />,
|
||||
StarOutlined: <StarOutlined />,
|
||||
HeartOutlined: <HeartOutlined />,
|
||||
CameraOutlined: <CameraOutlined />,
|
||||
RobotOutlined: <PlayCircleOutlined />,
|
||||
CalculatorOutlined: <CalculatorOutlined />,
|
||||
DollarOutlined: <DollarOutlined />,
|
||||
GiftOutlined: <GiftOutlined />,
|
||||
ThunderboltOutlined: <ThunderboltOutlined />,
|
||||
FireOutlined: <FireOutlined />,
|
||||
CloudOutlined: <CloudOutlined />,
|
||||
SmileOutlined: <SmileOutlined />,
|
||||
TrophyOutlined: <TrophyOutlined />,
|
||||
RocketOutlined: <RocketOutlined />,
|
||||
BulbOutlined: <BulbOutlined />,
|
||||
PictureOutlined: <PictureOutlined />,
|
||||
VideoCameraOutlined: <VideoCameraOutlined />,
|
||||
AudioOutlined: <AudioOutlined />,
|
||||
MailOutlined: <MailOutlined />,
|
||||
PhoneOutlined: <PhoneOutlined />,
|
||||
GlobalOutlined: <GlobalOutlined />,
|
||||
ShoppingCartOutlined: <ShoppingCartOutlined />,
|
||||
TeamOutlined: <TeamOutlined />,
|
||||
BarChartOutlined: <BarChartOutlined />,
|
||||
PieChartOutlined: <PieChartOutlined />,
|
||||
LineChartOutlined: <LineChartOutlined />,
|
||||
SecurityScanOutlined: <SecurityScanOutlined />,
|
||||
ApiOutlined: <ApiOutlined />,
|
||||
DatabaseOutlined: <DatabaseOutlined />,
|
||||
CloudServerOutlined: <CloudServerOutlined />,
|
||||
};
|
||||
|
||||
const EXPANDED_W = 240;
|
||||
@@ -83,8 +138,6 @@ const AppLayout: React.FC = () => {
|
||||
const [menuItems, setMenuItems] = useState<MenuConfig[]>([]);
|
||||
const [rechargeOptions, setRechargeOptions] = useState<any[]>([]);
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
|
||||
const [msgModalOpen, setMsgModalOpen] = useState(false);
|
||||
const [allNotifications, setAllNotifications] = useState<any[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [siteName, setSiteName] = useState('VideoGen.AI');
|
||||
const [siteLogo, setSiteLogo] = useState('');
|
||||
@@ -118,10 +171,9 @@ const AppLayout: React.FC = () => {
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const loadNotifications = () => {
|
||||
getNotifications().then(data => {
|
||||
setAllNotifications(data);
|
||||
setUnreadCount(data.filter((n: any) => !(n.isRead ?? n.is_read)).length);
|
||||
const loadUnreadCount = () => {
|
||||
getUnreadCount().then(count => {
|
||||
setUnreadCount(count);
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
@@ -204,7 +256,7 @@ const AppLayout: React.FC = () => {
|
||||
if (data.alipay) setPaymentMethod('alipay');
|
||||
else if (data.wechat) setPaymentMethod('wechat');
|
||||
}).catch(() => {});
|
||||
loadNotifications();
|
||||
loadUnreadCount();
|
||||
}, [user]);
|
||||
|
||||
const selectedKey = location.pathname.startsWith('/records') ? '/records' : location.pathname;
|
||||
@@ -212,7 +264,12 @@ const AppLayout: React.FC = () => {
|
||||
|
||||
const userMenuItems = [
|
||||
{ key: 'profile', icon: <UserOutlined />, label: `账号: ${user?.username}`, disabled: true },
|
||||
{ key: 'credits', icon: <WalletOutlined />, label: `积分余额: ${user?.credits ?? 0}`, disabled: true },
|
||||
{ key: 'credits', icon: <WalletOutlined />, label: `可用积分: ${user?.credits ?? 0}`, disabled: true },
|
||||
{ type: 'divider' as const },
|
||||
{ key: 'myCredits', icon: <WalletOutlined />, label: '积分明细' },
|
||||
{ key: 'orderRecords', icon: <FileTextOutlined />, label: '订单记录' },
|
||||
{ key: 'messages', icon: <BellOutlined />, label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` },
|
||||
// { key: 'recharge', icon: <PlusCircleOutlined />, label: '充值积分' },
|
||||
{ type: 'divider' as const },
|
||||
{ key: 'changePwd', icon: <LockOutlined />, label: '修改密码' },
|
||||
{ type: 'divider' as const },
|
||||
@@ -222,6 +279,10 @@ const AppLayout: React.FC = () => {
|
||||
const handleUserMenuClick = ({ key }: { key: string }) => {
|
||||
if (key === 'logout') { logout(); navigate('/login'); }
|
||||
else if (key === 'changePwd') { setPwdModalOpen(true); }
|
||||
else if (key === 'messages') { navigate('/messages'); }
|
||||
else if (key === 'recharge') { setRechargeModalOpen(true); }
|
||||
else if (key === 'myCredits') { navigate('/credit-records'); }
|
||||
else if (key === 'orderRecords') { navigate('/order-records'); }
|
||||
};
|
||||
|
||||
const handleChangePwd = async () => {
|
||||
@@ -347,45 +408,6 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Credits pill */}
|
||||
<div onClick={() => navigate('/credits')} style={{
|
||||
margin: collapsed ? '16px auto 4px' : '16px 14px 4px',
|
||||
padding: collapsed ? '10px' : '12px 14px',
|
||||
borderRadius: 12,
|
||||
background: 'linear-gradient(135deg, rgba(99,102,241,0.12), rgba(139,92,246,0.12))',
|
||||
border: '1px solid rgba(99,102,241,0.15)', cursor: 'pointer', transition: 'all 0.25s',
|
||||
textAlign: collapsed ? 'center' : 'left',
|
||||
}}>
|
||||
{collapsed ? (
|
||||
<WalletOutlined style={{ color: '#818cf8', fontSize: 18 }} />
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
||||
<WalletOutlined style={{ color: '#818cf8', fontSize: 13 }} />
|
||||
<span style={{ color: 'rgba(203,213,225,0.6)', fontSize: 12 }}>可用积分</span>
|
||||
</div>
|
||||
<span style={{ color: '#fff', fontSize: 22, fontWeight: 800 }}>{user?.credits ?? 0}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recharge button - opens modal */}
|
||||
<div onClick={() => setRechargeModalOpen(true)} style={{
|
||||
margin: collapsed ? '8px auto 4px' : '8px 14px 4px',
|
||||
padding: collapsed ? '10px' : '10px 14px',
|
||||
borderRadius: 12, cursor: 'pointer', textAlign: 'center',
|
||||
background: 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.2))',
|
||||
border: '1px solid rgba(99,102,241,0.25)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.3), rgba(139,92,246,0.3))'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.2))'; }}
|
||||
>
|
||||
<PlusOutlined style={{ color: '#818cf8', fontSize: 13 }} />
|
||||
{!collapsed && <span style={{ color: '#818cf8', fontSize: 13, fontWeight: 600 }}>充值积分</span>}
|
||||
</div>
|
||||
|
||||
{/* Menu */}
|
||||
<div style={{ flex: 1, padding: '8px 8px', overflow: 'auto' }}>
|
||||
{!collapsed && (
|
||||
@@ -415,13 +437,13 @@ const AppLayout: React.FC = () => {
|
||||
gap: collapsed ? 0 : 10,
|
||||
padding: collapsed ? '10px 0' : depth > 0 ? '8px 14px 8px 32px' : '10px 14px',
|
||||
borderRadius: 10, margin: '2px 0', cursor: 'pointer',
|
||||
fontSize: depth > 0 ? 13 : 14, fontWeight: isActive ? 600 : 400,
|
||||
fontSize: depth > 0 ? 12 : 13, fontWeight: isActive ? 600 : 400,
|
||||
color: isActive ? '#fff' : 'rgba(148,163,184,0.75)',
|
||||
background: isActive ? 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.15))' : 'transparent',
|
||||
border: isActive ? '1px solid rgba(99,102,241,0.2)' : '1px solid transparent',
|
||||
transition: 'all 0.2s ease',
|
||||
}}>
|
||||
<span style={{ fontSize: depth > 0 ? 15 : 17, flexShrink: 0 }}>{menuIcon}</span>
|
||||
<span style={{ fontSize: depth > 0 ? 14 : 16, flexShrink: 0 }}>{menuIcon}</span>
|
||||
{!collapsed && <span style={{ whiteSpace: 'nowrap' }}>{item.label}</span>}
|
||||
</div>
|
||||
);
|
||||
@@ -461,31 +483,23 @@ const AppLayout: React.FC = () => {
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Message button */}
|
||||
<div onClick={() => { setMsgModalOpen(true); loadNotifications(); }} style={{
|
||||
{/* Recharge button - opens modal */}
|
||||
<div onClick={() => setRechargeModalOpen(true)} style={{
|
||||
margin: collapsed ? '4px auto' : '4px 14px',
|
||||
padding: collapsed ? '10px' : '10px 14px',
|
||||
borderRadius: 12, cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
gap: collapsed ? 0 : 10,
|
||||
color: 'rgba(148,163,184,0.75)', fontSize: 14,
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
color: '#818cf8', fontSize: 13, fontWeight: 600,
|
||||
background: 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.2))',
|
||||
border: '1px solid rgba(99,102,241,0.25)',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.03)'; }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.3), rgba(139,92,246,0.3))'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.2))'; }}
|
||||
>
|
||||
<span style={{ position: 'relative' }}>
|
||||
<BellOutlined style={{ fontSize: 16 }} />
|
||||
{unreadCount > 0 && (
|
||||
<span style={{
|
||||
position: 'absolute', top: -6, right: -8,
|
||||
background: '#ef4444', color: '#fff', fontSize: 10, fontWeight: 700,
|
||||
borderRadius: 10, padding: '0 5px', lineHeight: '16px', minWidth: 16, textAlign: 'center',
|
||||
}}>{unreadCount > 99 ? '99+' : unreadCount}</span>
|
||||
)}
|
||||
</span>
|
||||
{!collapsed && <span>消息中心</span>}
|
||||
<PlusOutlined style={{ fontSize: 14 }} />
|
||||
{!collapsed && <span>充值积分</span>}
|
||||
</div>
|
||||
|
||||
{/* User block at bottom-left */}
|
||||
@@ -509,7 +523,7 @@ const AppLayout: React.FC = () => {
|
||||
<div style={{ color: '#e2e8f0', fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{user?.username}
|
||||
</div>
|
||||
<div style={{ color: 'rgba(148,163,184,0.5)', fontSize: 11 }}>查看账号</div>
|
||||
<div style={{ color: 'rgba(148,163,184,0.5)', fontSize: 11 }}>可用积分:{user?.credits || 0}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -894,117 +908,6 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Message Center Modal */}
|
||||
<Modal
|
||||
open={msgModalOpen}
|
||||
onCancel={() => { setMsgModalOpen(false); }}
|
||||
footer={null} width={520}
|
||||
closable={false}
|
||||
styles={{
|
||||
body: { padding: 0, borderRadius: 16, overflow: 'hidden' },
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
padding: '20px 24px',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{
|
||||
width: 36, height: 36, borderRadius: 10,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
backdropFilter: 'blur(4px)',
|
||||
}}>
|
||||
<BellOutlined style={{ fontSize: 18, color: '#fff' }} />
|
||||
</div>
|
||||
<span style={{ color: '#fff', fontSize: 16, fontWeight: 700 }}>消息中心</span>
|
||||
{unreadCount > 0 && (
|
||||
<span style={{
|
||||
background: 'rgba(255,255,255,0.25)', color: '#fff',
|
||||
fontSize: 11, fontWeight: 600, borderRadius: 10,
|
||||
padding: '2px 10px', backdropFilter: 'blur(4px)',
|
||||
}}>{unreadCount} 条未读</span>
|
||||
)}
|
||||
</div>
|
||||
<Button type="text" icon={<CloseOutlined style={{ color: 'rgba(255,255,255,0.8)', fontSize: 16 }} />}
|
||||
onClick={() => setMsgModalOpen(false)}
|
||||
style={{ color: '#fff' }} />
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div style={{ maxHeight: 460, overflow: 'auto', padding: '12px 16px 16px' }}>
|
||||
{allNotifications.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
||||
<BellOutlined style={{ fontSize: 40, color: '#cbd5e1', marginBottom: 12 }} />
|
||||
<div style={{ color: '#94a3b8', fontSize: 14 }}>暂无消息</div>
|
||||
</div>
|
||||
) : (
|
||||
allNotifications.map((n: any) => {
|
||||
const isRead = n.isRead ?? n.is_read;
|
||||
const typeConfig: Record<string, { color: string; bg: string; label: string }> = {
|
||||
system: { color: '#6366f1', bg: 'rgba(99,102,241,0.1)', label: '系统' },
|
||||
credit: { color: '#f59e0b', bg: 'rgba(245,158,11,0.1)', label: '积分' },
|
||||
promo: { color: '#8b5cf6', bg: 'rgba(139,92,246,0.1)', label: '活动' },
|
||||
};
|
||||
const tc = typeConfig[n.type] || typeConfig.system;
|
||||
return (
|
||||
<div key={n.id} style={{
|
||||
padding: '16px', borderRadius: 12, marginBottom: 8,
|
||||
background: isRead ? '#fff' : 'linear-gradient(135deg, rgba(99,102,241,0.03), rgba(139,92,246,0.03))',
|
||||
border: isRead ? '1px solid #f0f0f5' : '1px solid rgba(99,102,241,0.18)',
|
||||
cursor: isRead ? 'default' : 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
position: 'relative',
|
||||
}} onClick={async () => {
|
||||
if (!isRead) {
|
||||
await markNotificationRead(n.id);
|
||||
loadNotifications();
|
||||
}
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.boxShadow = '0 2px 12px rgba(0,0,0,0.04)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.boxShadow = 'none'; }}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{/* Type icon */}
|
||||
<div style={{
|
||||
width: 36, height: 36, borderRadius: 10, flexShrink: 0,
|
||||
background: tc.bg, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<BellOutlined style={{ color: tc.color, fontSize: 15 }} />
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{!isRead && <span style={{
|
||||
width: 7, height: 7, borderRadius: '50%',
|
||||
background: '#6366f1', flexShrink: 0,
|
||||
boxShadow: '0 0 6px rgba(99,102,241,0.4)',
|
||||
}} />}
|
||||
<span style={{ fontWeight: 600, fontSize: 14, color: '#1e293b' }}>{n.title}</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 11, color: '#94a3b8', flexShrink: 0 }}>
|
||||
{n.createdAt ? n.createdAt.replace('T', ' ').slice(0, 16) : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 13, color: '#64748b', lineHeight: 1.7,
|
||||
display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
||||
}}>{n.content}</div>
|
||||
<Tag color={tc.color} style={{ marginTop: 8, borderRadius: 6, border: 'none', fontSize: 11, padding: '1px 8px' }}>
|
||||
{tc.label}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<NotificationPopup />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user