1、修改前台左侧菜单显示

2、调整积分放在用户浮窗
3、消息改为页面列表显示
4、增加订单详情页面
5、调整新增页面的整体接口逻辑和风格
This commit is contained in:
2026-06-15 15:02:15 +08:00
parent 1862a660f6
commit 91a802e9fa
11 changed files with 614 additions and 202 deletions
+91 -188
View File
@@ -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>
);