([]);
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: , label: `账号: ${user?.username}`, disabled: true },
- { key: 'credits', icon: , label: `积分余额: ${user?.credits ?? 0}`, disabled: true },
+ { key: 'credits', icon: , label: `可用积分: ${user?.credits ?? 0}`, disabled: true },
+ { type: 'divider' as const },
+ { key: 'myCredits', icon: , label: '积分明细' },
+ { key: 'orderRecords', icon: , label: '订单记录' },
+ { key: 'messages', icon: , label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` },
+ // { key: 'recharge', icon: , label: '充值积分' },
{ type: 'divider' as const },
{ key: 'changePwd', icon: , 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('/user-center?tab=credits'); }
+ else if (key === 'orderRecords') { navigate('/user-center?tab=orders'); }
};
const handleChangePwd = async () => {
@@ -347,45 +408,6 @@ const AppLayout: React.FC = () => {
- {/* Credits pill */}
- 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 ? (
-
- ) : (
- <>
-
-
- 可用积分
-
-
{user?.credits ?? 0}
- >
- )}
-
-
- {/* Recharge button - opens modal */}
- 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))'; }}
- >
-
- {!collapsed &&
充值积分}
-
-
{/* Menu */}
{!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',
}}>
- 0 ? 15 : 17, flexShrink: 0 }}>{menuIcon}
+ 0 ? 14 : 16, flexShrink: 0 }}>{menuIcon}
{!collapsed && {item.label}}
);
@@ -461,31 +483,23 @@ const AppLayout: React.FC = () => {
})()}
- {/* Message button */}
- { setMsgModalOpen(true); loadNotifications(); }} style={{
+ {/* Recharge button - opens modal */}
+
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))'; }}
>
-
-
- {unreadCount > 0 && (
- {unreadCount > 99 ? '99+' : unreadCount}
- )}
-
- {!collapsed &&
消息中心}
+
+ {!collapsed &&
充值积分}
{/* User block at bottom-left */}
@@ -509,7 +523,7 @@ const AppLayout: React.FC = () => {
{user?.username}
-
查看账号
+
可用积分:{user?.credits || 0}
)}
@@ -894,117 +908,6 @@ const AppLayout: React.FC = () => {
- {/* Message Center Modal */}
- { setMsgModalOpen(false); }}
- footer={null} width={520}
- closable={false}
- styles={{
- body: { padding: 0, borderRadius: 16, overflow: 'hidden' },
- }}
- >
- {/* Header */}
-
-
-
-
-
-
消息中心
- {unreadCount > 0 && (
-
{unreadCount} 条未读
- )}
-
-
}
- onClick={() => setMsgModalOpen(false)}
- style={{ color: '#fff' }} />
-
-
- {/* List */}
-
- {allNotifications.length === 0 ? (
-
- ) : (
- allNotifications.map((n: any) => {
- const isRead = n.isRead ?? n.is_read;
- const typeConfig: Record
= {
- 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 (
- {
- 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'; }}
- >
-
- {/* Type icon */}
-
-
-
- {/* Content */}
-
-
-
- {!isRead && }
- {n.title}
-
-
- {n.createdAt ? n.createdAt.replace('T', ' ').slice(0, 16) : ''}
-
-
-
{n.content}
-
- {tc.label}
-
-
-
-
- );
- })
- )}
-
-
-
);
diff --git a/video-gen-app/src/pages/ConsumePage.tsx b/video-gen-app/src/pages/ConsumePage.tsx
index 0ed9147f..07e45ac1 100644
--- a/video-gen-app/src/pages/ConsumePage.tsx
+++ b/video-gen-app/src/pages/ConsumePage.tsx
@@ -31,7 +31,7 @@ const consumptionTypeConfig = {
// 表头配置
const columns = [
- { title: '序号', dataIndex: 'index', key: 'index', width: 80, fixed: 'left', render: (text: number) => {text} },
+ { title: '序号', dataIndex: 'index', key: 'index', width: 80, fixed: 'left' as const, render: (text: number) => {text} },
{ title: '消耗ID', dataIndex: 'id', key: 'id', ellipsis: true, render: (text: string) => {text} },
{ title: '授权ID', dataIndex: 'authorizationId', key: 'authorizationId', ellipsis: true },
{
@@ -52,7 +52,7 @@ const columns = [
render: (text: number) => {text} 元
},
{ 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 = () => {
diff --git a/video-gen-app/src/pages/CreditRecordsPage.tsx b/video-gen-app/src/pages/CreditRecordsPage.tsx
new file mode 100644
index 00000000..00feb02d
--- /dev/null
+++ b/video-gen-app/src/pages/CreditRecordsPage.tsx
@@ -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([]);
+ 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 = {
+ 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 (
+
+ {config.label}
+
+ );
+ },
+ },
+ {
+ title: '描述',
+ dataIndex: 'description',
+ key: 'description',
+ ellipsis: true,
+ },
+ {
+ title: '变动积分',
+ dataIndex: 'amount',
+ key: 'amount',
+ width: 120,
+ align: 'right' as const,
+ render: (amount: number) => (
+ 0 ? '#10b981' : '#ef4444', fontSize: 15 }}>
+ {amount > 0 ? '+' : ''}{amount}
+
+ ),
+ },
+ {
+ 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 (
+
+
+
+
+
+
+
+
+ 可用积分
+
+
+ {credits.toLocaleString()}
+
+
+
+
+
+
+
+
+ {records.filter((r: any) => r.amount > 0).reduce((sum: number, r: any) => sum + r.amount, 0).toLocaleString()}
+
+
+
+
+
+
+
+ {records.filter((r: any) => r.amount < 0).reduce((sum: number, r: any) => sum + Math.abs(r.amount), 0).toLocaleString()}
+
+
+
+
+
+
+
+
+ 积分明细
+
+
+
+ {records.length === 0 ? (
+ 暂无积分变动记录}
+ />
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
+
+export default CreditRecordsPage;
\ No newline at end of file
diff --git a/video-gen-app/src/pages/MessagesPage.tsx b/video-gen-app/src/pages/MessagesPage.tsx
new file mode 100644
index 00000000..199bb00e
--- /dev/null
+++ b/video-gen-app/src/pages/MessagesPage.tsx
@@ -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([]);
+ 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 = {
+ system: { color: '#6366f1', label: '系统', bg: 'rgba(99,102,241,0.1)', icon: },
+ credit: { color: '#f59e0b', label: '积分', bg: 'rgba(245,158,11,0.1)', icon: },
+ promo: { color: '#8b5cf6', label: '活动', bg: 'rgba(139,92,246,0.1)', icon: },
+ };
+ const config = typeConfig[text] || typeConfig.system;
+ return (
+
+ {config.icon}
+ {config.label}
+
+ );
+ },
+ },
+ {
+ 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 ? (
+ 已读
+ ) : (
+ 未读
+ );
+ },
+ },
+ {
+ 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 ? (
+
+ ) : null;
+ },
+ },
+ ];
+
+ return (
+
+
+
+
+ 消息中心
+ 未读 {unreadCount} 条
+
+
+
+ {notifications.length === 0 ? (
+ 暂无消息}
+ />
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
+
+export default MessagesPage;
\ No newline at end of file
diff --git a/video-gen-app/src/pages/OrderRecordsPage.tsx b/video-gen-app/src/pages/OrderRecordsPage.tsx
new file mode 100644
index 00000000..83f03f67
--- /dev/null
+++ b/video-gen-app/src/pages/OrderRecordsPage.tsx
@@ -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([]);
+
+ 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 (
+
+ {orderNo}
+
+ );
+ },
+ },
+ {
+ title: '支付方式',
+ key: 'paymentMethod',
+ width: 100,
+ render: (_, record: any) => {
+ const method = record.payment_method || record.paymentMethod || 'wechat';
+ const methodConfig: Record = {
+ alipay: { icon: , label: '支付宝', color: '#1677ff' },
+ wechat: { icon: , label: '微信支付', color: '#07c160' },
+ };
+ const config = methodConfig[method] || methodConfig.wechat;
+ return (
+
+ {config.icon}
+ {config.label}
+
+ );
+ },
+ },
+ {
+ title: '金额',
+ key: 'amount',
+ width: 100,
+ align: 'right' as const,
+ render: (_, record: any) => {
+ const amount = record.amount || record.total_amount || 0;
+ return (
+
+ ¥{amount}
+
+ );
+ },
+ },
+ {
+ title: '获得积分',
+ key: 'credits',
+ width: 100,
+ align: 'center' as const,
+ render: (_, record: any) => {
+ const credits = record.credits || record.credit_amount || 0;
+ return (
+
+ {credits}
+
+ );
+ },
+ },
+ {
+ title: '状态',
+ key: 'status',
+ width: 100,
+ render: (_, record: any) => {
+ const status = record.status || 'pending';
+ const statusConfig: Record = {
+ 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 (
+
+ {config.label}
+
+ );
+ },
+ },
+ {
+ 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 (
+
+
+
+
+ 订单记录
+ 共 {orders.length} 条记录
+
+
+
+ {orders.length === 0 ? (
+ 暂无订单记录}
+ />
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
+
+export default OrderRecordsPage;
\ No newline at end of file
diff --git a/video-gen-app/src/pages/UserCenterPage.tsx b/video-gen-app/src/pages/UserCenterPage.tsx
new file mode 100644
index 00000000..221b2abc
--- /dev/null
+++ b/video-gen-app/src/pages/UserCenterPage.tsx
@@ -0,0 +1,46 @@
+import React, { useState, useEffect } from 'react';
+import { Tabs } from 'antd';
+import { useSearchParams } from 'react-router-dom';
+import CreditRecordsPage from './CreditRecordsPage';
+import OrderRecordsPage from './OrderRecordsPage';
+
+const UserCenterPage: React.FC = () => {
+ const [searchParams] = useSearchParams();
+ const [activeTab, setActiveTab] = useState('credits');
+
+ useEffect(() => {
+ const tab = searchParams.get('tab') || 'credits';
+ if (tab === 'orders') {
+ setActiveTab('orders');
+ } else {
+ setActiveTab('credits');
+ }
+ }, [searchParams]);
+
+ const tabs = [
+ {
+ key: 'credits',
+ label: '积分明细',
+ },
+ {
+ key: 'orders',
+ label: '订单记录',
+ },
+ ];
+
+ return (
+
+
+ {activeTab === 'credits' && }
+ {activeTab === 'orders' && }
+
+ );
+};
+
+export default UserCenterPage;
\ No newline at end of file