1、修改前台左侧菜单显示
2、调整积分放在用户浮窗 3、消息改为页面列表显示 4、增加订单详情页面 5、调整新增页面的整体接口逻辑和风格
This commit is contained in:
@@ -324,9 +324,9 @@ async def _seed_data():
|
||||
|
||||
default_menus = [
|
||||
("/projects", "我的项目", "HomeOutlined", 0, "frontend"),
|
||||
("/records", "生成记录", "PlayCircleOutlined", 1, "frontend"),
|
||||
("/credits", "积分中心", "WalletOutlined", 2, "frontend"),
|
||||
("/conversation", "ai对话", "StarOutlined", 3, "frontend"),
|
||||
("/conversation", "AI创作", "StarOutlined", 1, "frontend"),
|
||||
("/records", "项目记录", "PlayCircleOutlined", 2, "frontend"),
|
||||
("/generated", "素材云", "StarOutlined", 3, "frontend"),
|
||||
]
|
||||
for path, label, icon, order, target in default_menus:
|
||||
existing = await db.execute(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class RechargeRequest(BaseModel):
|
||||
@@ -14,5 +15,7 @@ class PaymentOrderOut(BaseModel):
|
||||
payment_method: str
|
||||
status: str
|
||||
qr_url: str | None = None # Alipay QR code URL (transient, not persisted)
|
||||
created_at: datetime
|
||||
paid_at: datetime | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
Generated
+8
-8
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user