From 91a802e9fa0cac6dd48abd350f5cedbb5597346d Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 15 Jun 2026 15:02:15 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E6=94=B9=E5=89=8D=E5=8F=B0?= =?UTF-8?q?=E5=B7=A6=E4=BE=A7=E8=8F=9C=E5=8D=95=E6=98=BE=E7=A4=BA=202?= =?UTF-8?q?=E3=80=81=E8=B0=83=E6=95=B4=E7=A7=AF=E5=88=86=E6=94=BE=E5=9C=A8?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=B5=AE=E7=AA=97=203=E3=80=81=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=94=B9=E4=B8=BA=E9=A1=B5=E9=9D=A2=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=204=E3=80=81=E5=A2=9E=E5=8A=A0=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2=205=E3=80=81?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=B0=E5=A2=9E=E9=A1=B5=E9=9D=A2=E7=9A=84?= =?UTF-8?q?=E6=95=B4=E4=BD=93=E6=8E=A5=E5=8F=A3=E9=80=BB=E8=BE=91=E5=92=8C?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-api/app/main.py | 6 +- video-gen-api/app/schemas/payment.py | 3 + video-gen-app/package-lock.json | 16 +- video-gen-app/package.json | 2 +- video-gen-app/src/App.tsx | 7 + .../src/components/Layout/AppLayout.tsx | 279 ++++++------------ video-gen-app/src/pages/ConsumePage.tsx | 4 +- video-gen-app/src/pages/CreditRecordsPage.tsx | 157 ++++++++++ video-gen-app/src/pages/MessagesPage.tsx | 149 ++++++++++ video-gen-app/src/pages/OrderRecordsPage.tsx | 158 ++++++++++ video-gen-app/src/pages/UserCenterPage.tsx | 35 +++ 11 files changed, 614 insertions(+), 202 deletions(-) create mode 100644 video-gen-app/src/pages/CreditRecordsPage.tsx create mode 100644 video-gen-app/src/pages/MessagesPage.tsx create mode 100644 video-gen-app/src/pages/OrderRecordsPage.tsx create mode 100644 video-gen-app/src/pages/UserCenterPage.tsx diff --git a/video-gen-api/app/main.py b/video-gen-api/app/main.py index a9049055..d93f3727 100644 --- a/video-gen-api/app/main.py +++ b/video-gen-api/app/main.py @@ -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( diff --git a/video-gen-api/app/schemas/payment.py b/video-gen-api/app/schemas/payment.py index 135b4172..6ba5fc8c 100644 --- a/video-gen-api/app/schemas/payment.py +++ b/video-gen-api/app/schemas/payment.py @@ -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} diff --git a/video-gen-app/package-lock.json b/video-gen-app/package-lock.json index 00e61fb0..00f0f35a 100644 --- a/video-gen-app/package-lock.json +++ b/video-gen-app/package-lock.json @@ -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", diff --git a/video-gen-app/package.json b/video-gen-app/package.json index cc5a4029..dfba62c1 100644 --- a/video-gen-app/package.json +++ b/video-gen-app/package.json @@ -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", diff --git a/video-gen-app/src/App.tsx b/video-gen-app/src/App.tsx index 86ddce4e..88eea2d6 100644 --- a/video-gen-app/src/App.tsx +++ b/video-gen-app/src/App.tsx @@ -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 = () => { } /> } /> } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/video-gen-app/src/components/Layout/AppLayout.tsx b/video-gen-app/src/components/Layout/AppLayout.tsx index fffe327f..3cbb0e74 100644 --- a/video-gen-app/src/components/Layout/AppLayout.tsx +++ b/video-gen-app/src/components/Layout/AppLayout.tsx @@ -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 = { HomeOutlined: , - DashboardOutlined:, - CodeOutlined:, + DashboardOutlined: , + CodeOutlined: , PlayCircleOutlined: , WalletOutlined: , - SettingOutlined: , - BellOutlined: , + SettingOutlined: , + BellOutlined: , UserOutlined: , - AppstoreOutlined: , - FileTextOutlined: , - StarOutlined: , - HeartOutlined: , - CameraOutlined: , + AppstoreOutlined: , + FileTextOutlined: , + StarOutlined: , + HeartOutlined: , + CameraOutlined: , + RobotOutlined: , + CalculatorOutlined: , + DollarOutlined: , + GiftOutlined: , + ThunderboltOutlined: , + FireOutlined: , + CloudOutlined: , + SmileOutlined: , + TrophyOutlined: , + RocketOutlined: , + BulbOutlined: , + PictureOutlined: , + VideoCameraOutlined: , + AudioOutlined: , + MailOutlined: , + PhoneOutlined: , + GlobalOutlined: , + ShoppingCartOutlined: , + TeamOutlined: , + BarChartOutlined: , + PieChartOutlined: , + LineChartOutlined: , + SecurityScanOutlined: , + ApiOutlined: , + DatabaseOutlined: , + CloudServerOutlined: , }; const EXPANDED_W = 240; @@ -83,8 +138,6 @@ const AppLayout: React.FC = () => { const [menuItems, setMenuItems] = useState([]); const [rechargeOptions, setRechargeOptions] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState>({}); - const [msgModalOpen, setMsgModalOpen] = useState(false); - const [allNotifications, setAllNotifications] = useState([]); 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('/credit-records'); } + else if (key === 'orderRecords') { navigate('/order-records'); } }; 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} 条未读 - )} -
-
- - {/* 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..91544ee1 --- /dev/null +++ b/video-gen-app/src/pages/UserCenterPage.tsx @@ -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 ( +
+ + {activeTab === 'credits' && } + {activeTab === 'orders' && } +
+ ); +}; + +export default UserCenterPage; \ No newline at end of file