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
+8 -8
View File
@@ -8,7 +8,7 @@
"name": "video-gen-app",
"version": "0.0.0",
"dependencies": {
"@ant-design/icons": "^6.2.2",
"@ant-design/icons": "^6.2.5",
"antd": "^6.3.7",
"dayjs": "^1.11.20",
"plyr-react": "^6.0.0",
@@ -103,14 +103,14 @@
}
},
"node_modules/@ant-design/icons": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.2.tgz",
"integrity": "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ==",
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.5.tgz",
"integrity": "sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^8.0.1",
"@ant-design/icons-svg": "^4.4.2",
"@rc-component/util": "^1.10.1",
"@rc-component/util": "^1.11.0",
"clsx": "^2.1.1"
},
"engines": {
@@ -1430,9 +1430,9 @@
}
},
"node_modules/@rc-component/util": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.1.tgz",
"integrity": "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.11.1.tgz",
"integrity": "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg==",
"license": "MIT",
"dependencies": {
"is-mobile": "^5.0.0",
+1 -1
View File
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^6.2.2",
"@ant-design/icons": "^6.2.5",
"antd": "^6.3.7",
"dayjs": "^1.11.20",
"plyr-react": "^6.0.0",
+7
View File
@@ -8,6 +8,10 @@ import ProjectsPage from './pages/ProjectsPage';
import GeneratePage from './pages/GeneratePage';
import RecordsPage from './pages/RecordsPage';
import CreditsPage from './pages/CreditsPage';
import CreditRecordsPage from './pages/CreditRecordsPage';
import OrderRecordsPage from './pages/OrderRecordsPage';
import MessagesPage from './pages/MessagesPage';
import UserCenterPage from './pages/UserCenterPage';
import GenerateConver from './pages/GenerateConver';
import InitialReplication from './pages/InitialReplication';
import InitialInfo from './pages/InitialInfo';
@@ -94,6 +98,9 @@ const App = () => {
<Route path="projects/:projectId/generate" element={<GeneratePage />} />
<Route path="records" element={<RecordsPage />} />
<Route path="credits" element={<CreditsPage />} />
<Route path="credit-records" element={<UserCenterPage />} />
<Route path="order-records" element={<UserCenterPage />} />
<Route path="messages" element={<MessagesPage />} />
<Route path="conversation" element={<GenerateConver />} />
<Route path="initial" element={<InitialReplication />} />
<Route path="initial/:creatID/initialinfo" element={<InitialInfo />} />
+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>
);
+2 -2
View File
@@ -31,7 +31,7 @@ const consumptionTypeConfig = {
// 表头配置
const columns = [
{ title: '序号', dataIndex: 'index', key: 'index', width: 80, fixed: 'left', render: (text: number) => <span style={{ color: '#94a3b8' }}>{text}</span> },
{ title: '序号', dataIndex: 'index', key: 'index', width: 80, fixed: 'left' as const, render: (text: number) => <span style={{ color: '#94a3b8' }}>{text}</span> },
{ title: '消耗ID', dataIndex: 'id', key: 'id', ellipsis: true, render: (text: string) => <span style={{ fontWeight: 500, color: '#1e293b' }}>{text}</span> },
{ title: '授权ID', dataIndex: 'authorizationId', key: 'authorizationId', ellipsis: true },
{
@@ -52,7 +52,7 @@ const columns = [
render: (text: number) => <span style={{ color: '#ef4444', fontWeight: 500 }}>{text} </span>
},
{ title: '消耗描述', dataIndex: 'description', key: 'description', ellipsis: true },
{ title: '消耗时间', dataIndex: 'createdAt', key: 'createdAt', width: 160, fixed: 'right' },
{ title: '消耗时间', dataIndex: 'createdAt', key: 'createdAt', width: 160, fixed: 'right' as const },
];
const ConsumePage: React.FC = () => {
@@ -0,0 +1,157 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, Empty, Spin, Typography, Row, Col } from 'antd';
import { WalletOutlined, PlusCircleOutlined, ThunderboltOutlined } from '@ant-design/icons';
import { getCredits } from '../api';
const CreditRecordsPage: React.FC = () => {
const [loading, setLoading] = useState(true);
const [records, setRecords] = useState<any[]>([]);
const [credits, setCredits] = useState(0);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const data = await getCredits();
setRecords(data.records || []);
setCredits(data.credits || 0);
} catch {
setRecords([]);
}
setLoading(false);
};
const columns = [
{
title: '变动类型',
dataIndex: 'type',
key: 'type',
width: 120,
render: (text: string) => {
const typeConfig: Record<string, { color: string; label: string; bg: string }> = {
recharge: { color: '#10b981', label: '充值', bg: 'rgba(16,185,129,0.1)' },
consume: { color: '#ef4444', label: '消费', bg: 'rgba(239,68,68,0.1)' },
admin: { color: '#f59e0b', label: '管理员调整', bg: 'rgba(245,158,11,0.1)' },
refund: { color: '#8b5cf6', label: '退款', bg: 'rgba(139,92,246,0.1)' },
};
const config = typeConfig[text] || { color: '#64748b', label: text, bg: 'rgba(100,116,139,0.1)' };
return (
<Tag color={config.color} style={{ background: config.bg, fontWeight: 500, padding: '4px 12px' }}>
{config.label}
</Tag>
);
},
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{
title: '变动积分',
dataIndex: 'amount',
key: 'amount',
width: 120,
align: 'right' as const,
render: (amount: number) => (
<Typography.Text strong style={{ fontWeight: 700, color: amount > 0 ? '#10b981' : '#ef4444', fontSize: 15 }}>
{amount > 0 ? '+' : ''}{amount}
</Typography.Text>
),
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
render: (_, record: any) => {
const createdAt = record.created_at || record.createdAt || record.create_time || record.createTime;
return createdAt ? new Date(createdAt).toLocaleString('zh-CN') : '-';
},
},
];
return (
<div>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={8}>
<div style={{
borderRadius: 16, padding: 24,
background: 'linear-gradient(135deg, #f59e0b 0%, #ea580c 100%)',
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: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<WalletOutlined style={{ color: 'rgba(255,255,255,0.8)', fontSize: 16 }} />
<span style={{ color: 'rgba(255,255,255,0.8)', fontSize: 14 }}></span>
</div>
<div style={{ color: '#fff', fontSize: 38, fontWeight: 800, lineHeight: 1 }}>
{credits.toLocaleString()}
</div>
</div>
</div>
</Col>
<Col xs={12} sm={8}>
<div style={{
borderRadius: 16, padding: 24, background: '#fff', border: '1px solid #f0f0f5',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'rgba(16,185,129,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<PlusCircleOutlined style={{ color: '#10b981', fontSize: 14 }} />
</div>
<span style={{ color: '#94a3b8', fontSize: 14 }}></span>
</div>
<div style={{ color: '#10b981', fontSize: 28, fontWeight: 800 }}>
{records.filter((r: any) => r.amount > 0).reduce((sum: number, r: any) => sum + r.amount, 0).toLocaleString()}
</div>
</div>
</Col>
<Col xs={12} sm={8}>
<div style={{
borderRadius: 16, padding: 24, background: '#fff', border: '1px solid #f0f0f5',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'rgba(239,68,68,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ThunderboltOutlined style={{ color: '#ef4444', fontSize: 14 }} />
</div>
<span style={{ color: '#94a3b8', fontSize: 14 }}></span>
</div>
<div style={{ color: '#ef4444', fontSize: 28, fontWeight: 800 }}>
{records.filter((r: any) => r.amount < 0).reduce((sum: number, r: any) => sum + Math.abs(r.amount), 0).toLocaleString()}
</div>
</div>
</Col>
</Row>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<ThunderboltOutlined style={{ color: '#f59e0b', fontSize: 16 }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
</div>
<div style={{ borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }}>
<Spin spinning={loading}>
{records.length === 0 ? (
<Empty
description={<span style={{ color: '#94a3b8' }}></span>}
/>
) : (
<Table
dataSource={records}
columns={columns}
rowKey="id"
pagination={{ pageSize: 10, size: 'small' }}
bordered={false}
/>
)}
</Spin>
</div>
</div>
</div>
);
};
export default CreditRecordsPage;
+149
View File
@@ -0,0 +1,149 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, Empty, Spin, Typography, Button } from 'antd';
import { BellOutlined, SettingOutlined, WalletOutlined, GiftOutlined } from '@ant-design/icons';
import { getNotifications, markNotificationRead } from '../api';
const MessagesPage: React.FC = () => {
const [loading, setLoading] = useState(true);
const [notifications, setNotifications] = useState<any[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const data = await getNotifications();
setNotifications(data || []);
setUnreadCount(data.filter((n: any) => !(n.isRead ?? n.is_read)).length);
} catch {
setNotifications([]);
}
setLoading(false);
};
const handleMarkRead = async (id: string) => {
try {
await markNotificationRead(id);
loadData();
} catch { /* ignore */ }
};
const columns = [
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100,
render: (text: string) => {
const typeConfig: Record<string, { color: string; label: string; bg: string; icon: React.ReactNode }> = {
system: { color: '#6366f1', label: '系统', bg: 'rgba(99,102,241,0.1)', icon: <SettingOutlined style={{ fontSize: 14 }} /> },
credit: { color: '#f59e0b', label: '积分', bg: 'rgba(245,158,11,0.1)', icon: <WalletOutlined style={{ fontSize: 14 }} /> },
promo: { color: '#8b5cf6', label: '活动', bg: 'rgba(139,92,246,0.1)', icon: <GiftOutlined style={{ fontSize: 14 }} /> },
};
const config = typeConfig[text] || typeConfig.system;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ color: config.color }}>{config.icon}</span>
<span style={{ fontSize: 13, color: config.color, fontWeight: 500 }}>{config.label}</span>
</div>
);
},
},
{
title: '标题',
dataIndex: 'title',
key: 'title',
ellipsis: true,
},
{
title: '内容',
dataIndex: 'content',
key: 'content',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'isRead',
key: 'isRead',
width: 80,
render: (text: boolean, record: any) => {
const isRead = text ?? record.is_read;
return isRead ? (
<Tag color="gray" style={{ background: 'rgba(148,163,184,0.1)' }}></Tag>
) : (
<Tag color="purple" style={{ background: 'rgba(139,92,246,0.1)' }}></Tag>
);
},
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (text: string) => {
if (!text) return '-';
const date = new Date(text);
const now = new Date();
const diff = now.getTime() - date.getTime();
const hours = Math.floor(diff / (1000 * 60 * 60));
if (hours < 1) return '刚刚';
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}天前`;
return date.toLocaleDateString('zh-CN');
},
},
{
title: '操作',
key: 'action',
width: 100,
render: (_, record: any) => {
const isRead = record.isRead ?? record.is_read;
return !isRead ? (
<Button
type="text"
size="small"
onClick={() => handleMarkRead(record.id)}
style={{ color: '#6366f1' }}
>
</Button>
) : null;
},
},
];
return (
<div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<BellOutlined style={{ color: '#6366f1', fontSize: 16 }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
<span style={{ color: '#94a3b8', fontSize: 13 }}> {unreadCount} </span>
</div>
<div style={{ borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }}>
<Spin spinning={loading}>
{notifications.length === 0 ? (
<Empty
description={<span style={{ color: '#94a3b8' }}></span>}
/>
) : (
<Table
dataSource={notifications}
columns={columns}
rowKey="id"
pagination={{ pageSize: 10, size: 'small' }}
bordered={false}
/>
)}
</Spin>
</div>
</div>
</div>
);
};
export default MessagesPage;
@@ -0,0 +1,158 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, Empty, Spin, Typography } from 'antd';
import { FileTextOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons';
import { getPaymentOrders } from '../api';
const OrderRecordsPage: React.FC = () => {
const [loading, setLoading] = useState(true);
const [orders, setOrders] = useState<any[]>([]);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const data = await getPaymentOrders();
setOrders(data || []);
} catch {
setOrders([]);
}
setLoading(false);
};
const columns = [
{
title: '订单号',
key: 'orderNo',
width: 200,
render: (_, record: any) => {
const orderNo = record.order_no || record.orderNo || record.id;
return (
<span style={{ fontFamily: 'monospace', fontSize: 13, color: '#64748b' }}>
{orderNo}
</span>
);
},
},
{
title: '支付方式',
key: 'paymentMethod',
width: 100,
render: (_, record: any) => {
const method = record.payment_method || record.paymentMethod || 'wechat';
const methodConfig: Record<string, { icon: React.ReactNode; label: string; color: string }> = {
alipay: { icon: <AlipayCircleOutlined style={{ fontSize: 16 }} />, label: '支付宝', color: '#1677ff' },
wechat: { icon: <WechatOutlined style={{ fontSize: 16 }} />, label: '微信支付', color: '#07c160' },
};
const config = methodConfig[method] || methodConfig.wechat;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ color: config.color }}>{config.icon}</span>
<span style={{ fontSize: 13, color: '#374151' }}>{config.label}</span>
</div>
);
},
},
{
title: '金额',
key: 'amount',
width: 100,
align: 'right' as const,
render: (_, record: any) => {
const amount = record.amount || record.total_amount || 0;
return (
<Typography.Text strong style={{ fontWeight: 700, fontSize: 14, color: '#1e293b' }}>
¥{amount}
</Typography.Text>
);
},
},
{
title: '获得积分',
key: 'credits',
width: 100,
align: 'center' as const,
render: (_, record: any) => {
const credits = record.credits || record.credit_amount || 0;
return (
<Typography.Text strong style={{ fontWeight: 600, fontSize: 14, color: '#f59e0b' }}>
{credits}
</Typography.Text>
);
},
},
{
title: '状态',
key: 'status',
width: 100,
render: (_, record: any) => {
const status = record.status || 'pending';
const statusConfig: Record<string, { color: string; label: string; bg: string }> = {
pending: { color: '#f59e0b', label: '待支付', bg: 'rgba(245,158,11,0.1)' },
paid: { color: '#10b981', label: '已支付', bg: 'rgba(16,185,129,0.1)' },
refunded: { color: '#94a3b8', label: '已退款', bg: 'rgba(148,163,184,0.1)' },
failed: { color: '#ef4444', label: '支付失败', bg: 'rgba(239,68,68,0.1)' },
cancelled: { color: '#64748b', label: '已取消', bg: 'rgba(100,116,139,0.1)' },
processing: { color: '#0ea5e9', label: '处理中', bg: 'rgba(14,165,233,0.1)' },
};
const config = statusConfig[status] || { color: '#64748b', label: status, bg: 'rgba(100,116,139,0.1)' };
return (
<Tag color={config.color} style={{ background: config.bg, fontWeight: 500, padding: '4px 12px' }}>
{config.label}
</Tag>
);
},
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
render: (_, record: any) => {
const createdAt = record.created_at || record.createdAt;
return createdAt ? new Date(createdAt).toLocaleString('zh-CN') : '-';
},
},
{
title: '支付时间',
key: 'paidAt',
width: 180,
render: (_, record: any) => {
const paidAt = record.paid_at || record.paidAt;
return paidAt ? new Date(paidAt).toLocaleString('zh-CN') : '-';
},
},
];
return (
<div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<FileTextOutlined style={{ color: '#0ea5e9', fontSize: 16 }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
<span style={{ color: '#94a3b8', fontSize: 13 }}> {orders.length} </span>
</div>
<div style={{ borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }}>
<Spin spinning={loading}>
{orders.length === 0 ? (
<Empty
description={<span style={{ color: '#94a3b8' }}></span>}
/>
) : (
<Table
dataSource={orders}
columns={columns}
rowKey="order_no"
pagination={{ pageSize: 10, size: 'small' }}
bordered={false}
/>
)}
</Spin>
</div>
</div>
</div>
);
};
export default OrderRecordsPage;
@@ -0,0 +1,35 @@
import React, { useState } from 'react';
import { Tabs } from 'antd';
import CreditRecordsPage from './CreditRecordsPage';
import OrderRecordsPage from './OrderRecordsPage';
const UserCenterPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('credits');
const tabs = [
{
key: 'credits',
label: '积分明细',
},
{
key: 'orders',
label: '订单记录',
},
];
return (
<div>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabs}
style={{ marginBottom: 24 }}
size="large"
/>
{activeTab === 'credits' && <CreditRecordsPage />}
{activeTab === 'orders' && <OrderRecordsPage />}
</div>
);
};
export default UserCenterPage;